| index 5b52049c7..d38c2d0bc 100644 |
| --- a/internal/sim/hostlife/witness_test.go |
| +++ b/internal/sim/hostlife/witness_test.go |
| @@ -125,6 +125,10 @@ var witnesses = []witness{ |
| "a clean stop with an upload still in flight: closed.json is written only once the upload returned; written at once, the marker vouches for an upload that may still fail or land late (claim rev 6.6 §6.4a)", wCloseWaits}, |
| {"newest-l0", "newest-l0", "newest level-0", |
| "the local position runs ahead of the uploads; two compactions and snapshot rotations later, retention below the previous snapshot would remove the newest uploaded level-0 file, which litestream never does (R7-3)", wNewestL0}, |
| + {"evidence-whole-tree", "evidence-whole-tree", "INV-1", |
| + "an adopted legacy host runs a descendant outside the cgroup; the move's stop evidence must cover the whole tree, or the escaped descendant survives the cut and two boxes run the host (§6.1, J-D3)", wEvidenceWholeTree}, |
| + {"timer-freeze", "timer-freeze", "stuck", |
| + "the source has a registered timer; the move stops and holds it; the runner's refused deliveries while held must not advance its bookkeeping, or a clean rollback's digest check finds the timers changed and the source is stuck (J-N14)", wTimerFreeze}, |
| {"build-completion", "build-completion", "before its snapshot", |
| "a push's build with its snapshot PUT, then its anchor PUT, held, and the agent crashing in between: generation.json is published only once both are acknowledged; published from the intent it names artifacts that have not landed (R7-4)", wBuildCrashes}, |
| } |
| @@ -1683,3 +1687,38 @@ func wBuildCrashes(t *testing.T, w *world) { |
| w.replicate(b) |
| w.finish(t) |
| } |
| + |
| +// wEvidenceWholeTree (§6.1): a legacy host adopted with a descendant that |
| +// escaped the cgroup; the move cuts. |
| +func wEvidenceWholeTree(t *testing.T, w *world) { |
| + b0, b1 := w.boxes[0], w.boxes[1] |
| + w.legacy(b0, 1) |
| + w.tick(func() { w.reconcile(b0) }) // adoption |
| + if w.claim == nil { |
| + t.Fatalf("witness: not adopted") |
| + } |
| + w.reach(t, 400, "adoption settled", func() bool { return w.claim.oblFor("adopt") == 0 }, w.actAll(), w.recoveryPass) |
| + if !hasMain(b0) { |
| + w.recovered(t, b0, "running after adoption") |
| + } |
| + b0.Procs = append(b0.Procs, &proc{Main: false, Strict: false, Inc: 1, Box: b0.ID}) // an escaped descendant |
| + w.moveTo(b1.Region) |
| + w.finish(t) |
| +} |
| + |
| +// wTimerFreeze (J-N14): a timer registered on the source, a move that |
| +// holds the row, refused deliveries, then a clean rollback whose verify |
| +// compares the timer digest with the one recorded at the stop. |
| +func wTimerFreeze(t *testing.T, w *world) { |
| + src, tgt := w.boxes[0], w.boxes[1] |
| + w.serve(src, 1, app0) |
| + w.nextW++ |
| + src.Timers = []int{w.nextW} |
| + w.acks = append(w.acks, ack{W: w.nextW, Inc: 1, Box: src.ID, At: w.now, Kind: "timer"}) |
| + w.landProbed(t, src, tgt) // the source is stopped and its row held |
| + for i := 0; i < 3; i++ { |
| + w.runner(src) // deliveries refused while the row is held |
| + } |
| + w.rollback(w.claim, "settle check failed") // clean: the target wrote nothing |
| + w.finish(t) |
| +} |