R-1187 (c): refuse a .fab import over an installed app that is not running (it merged old and bundle data); R-1189 (d): the off-site proof + integrity check run after a catch-up; unreleased
gates / gates (push) Successful in 1m54s

This commit is contained in:
2026-10-22 17:27:12 +02:00
parent 6a17c95815
commit d5f88a2182
7 changed files with 175 additions and 5 deletions
+12 -4
View File
@@ -224,10 +224,14 @@ type CatchUp struct {
Legs map[Leg]func(context.Context) error // the leg bodies — WITHOUT the update leg
QuiesceBusy func() bool // a whole-guest backup holds the apps now
Event func(missedAt time.Time) // the household's timeline line
Logger *log.Logger
Delay time.Duration
Loc *time.Location
Now func() time.Time
// After (R-1189 d) runs once after the legs of a catch-up that ran: the off-site proof (05:30) and the integrity
// check (06:00) are daily jobs the scheduler never runs late, so a box that is off at those hours was never
// checked — the catch-up is where such a box comes back. nil = nothing.
After func(ctx context.Context)
Logger *log.Logger
Delay time.Duration
Loc *time.Location
Now func() time.Time
// Sleep waits d or until ctx ends (false). A seam for tests.
Sleep func(ctx context.Context, d time.Duration) bool
@@ -336,6 +340,10 @@ func (c *CatchUp) run(ctx context.Context, why string) {
names = append(names, string(leg))
}
c.Ledger.markCatchUp(c.now())
if c.After != nil {
c.Logger.Printf("[INFO] [catch-up] running the after-catch-up checks (the off-site proof and the due-gated integrity check, R-1189 d)")
c.After(ctx)
}
sort.Strings(names)
c.Logger.Printf("[INFO] [catch-up] done: %s in %s (missed at %s)", strings.Join(names, ", "),
c.now().Sub(start).Round(time.Second), lastW.In(c.Loc).Format("2006-01-02 15:04"))
@@ -0,0 +1,45 @@
package nightchain
import (
"context"
"sync"
"testing"
)
// R-1189 (d): the after-catch-up checks run once, AFTER every leg of the catch-up (the off-site proof and the
// integrity check are daily jobs the scheduler never runs late; the catch-up is where a box off at 05:30–06:00 comes
// back). A box with nothing missed never runs them from here (its 05:30/06:00 jobs do).
// RED-PROOF: with the After call removed from run(), the first assertion fails — seen failing 2026-10-22.
func TestR1189d_TheChecksRunAfterTheCatchUpLegs(t *testing.T) {
h := newHarness(t, at(1, 12, 0), at(5, 9, 0))
h.l.MarkEnded(LegDBDump, at(4, 2, 31))
h.l.MarkEnded(LegTier2, at(4, 3, 31))
h.l.MarkEnded(LegOffsite, at(4, 4, 20))
var mu sync.Mutex
var after []int // how many legs had run when After was called
h.c.After = func(context.Context) {
mu.Lock()
after = append(after, len(h.ran))
mu.Unlock()
}
if legs := h.c.Evaluate(context.Background(), "start"); len(legs) != 3 {
t.Fatalf("no catch-up scheduled: %v", legs)
}
h.wait(t)
mu.Lock()
defer mu.Unlock()
if len(after) != 1 || after[0] != 3 {
t.Fatalf("After calls %v, want exactly one, after all 3 legs", after)
}
h2 := newHarness(t, at(1, 12, 0), at(5, 11, 0))
h2.l.MarkEnded(LegDBDump, at(5, 2, 31))
h2.l.MarkEnded(LegTier2, at(5, 3, 31))
h2.l.MarkEnded(LegOffsite, at(5, 4, 20))
called := false
h2.c.After = func(context.Context) { called = true }
h2.c.Evaluate(context.Background(), "start")
if called {
t.Fatal("no catch-up ran, so the after-catch-up checks must not run")
}
}