carlos / platform Public

host: the cut's crash matrix: after the evidence, after the final round, after the seal and between the two snapshot PUTs, each replayed by a fresh worker (Task 5 fix round 1, ruling 6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6b1e3e7ed8285647e4d413b7fb9ee2287d5199cf parent b049b85
1 file changed, +95 −0
  • internal/host/movescut_test.go +95 −0
diff --git a/internal/host/movescut_test.go b/internal/host/movescut_test.go
index ecd90aeb2..d4f79e049 100644
--- a/internal/host/movescut_test.go
+++ b/internal/host/movescut_test.go
@@ -1375,3 +1375,98 @@ func TestArchiveSealStopsOnALedgerFailure(t *testing.T) {
t.Fatalf("ledger bound %v, seal bound %v (was %v)", after.LeaseUntil, seal.LeaseUntil, before.LeaseUntil)
}
}
+
+// Ruling 6: the crash matrix for the side effects the review found
+// unexercised. Each point crashes once; the durable state a fresh worker
+// would find is asserted; the fresh worker replays to the flip, with one
+// report, a seal naming the archive at the record's N, and the ledger
+// carrying the seal's bound.
+func TestCutCrashMatrix(t *testing.T) {
+ ctx := context.Background()
+ lane := moves.ReplicaPrefix(p1Host, p1ID)
+ for _, point := range []string{"after-evidence", "after-final-round", "after-seal", "archive-sealed-snapshot"} {
+ t.Run(point, func(t *testing.T) {
+ h := newCutHarness(t)
+ h.ready(t)
+ h.writeOnStop(t) // so the archive is the cut's own build
+ h.crashAt = point
+ h.cut(t)
+ h.crashPass(t)
+ m := h.move(t)
+ c := h.claim(t)
+ j, err := moves.ReadJournal(h.cfg.DataDir, p1Host, p1ID)
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, _, reportErr := h.s.Get(ctx, h.stoppedKey())
+ _, sealed := c.Op.Journal[hostclaim.JournalSeal]
+ switch point {
+ case "after-evidence":
+ // stopping: the evidence journalled, execution still the
+ // source's, no report, no hold given up.
+ if m.CutPhase != provision.CutStopping || !j.Done(cutStepEvidence) || c.Op.Exec != "ie-box" || !errors.Is(reportErr, os.ErrNotExist) {
+ t.Fatalf("durable state: %+v, evidence %v, exec %q, report %v", m, j.Done(cutStepEvidence), c.Op.Exec, reportErr)
+ }
+ case "after-final-round":
+ // stopped: execution given up, the report published, the
+ // lane holding the final round's copy, and no seal yet.
+ if m.CutPhase != provision.CutStopped || c.Op.Exec != "" || c.Op.Phase != hostclaim.PhaseMoving || reportErr != nil || sealed {
+ t.Fatalf("durable state: %+v, claim %+v, report %v", m, c.Op, reportErr)
+ }
+ if _, ok := h.f.buckets["data-uk"][lane+lastTXKey]; !ok {
+ t.Fatal("the final round's copy is not in the lane")
+ }
+ case "after-seal":
+ // stopped: the claim sealed with a seal naming the replica and
+ // the timers but no files; the ledger without a bound yet.
+ if m.CutPhase != provision.CutStopped || c.Op.Phase != hostclaim.PhaseSealed || !sealed {
+ t.Fatalf("durable state: %+v, claim %+v", m, c.Op)
+ }
+ if seal := h.seal(t); seal.HasFiles() || seal.N != 3 || len(seal.Timers) != 1 {
+ t.Fatalf("seal: %+v", seal)
+ }
+ if l := h.ledger(t); !l.LeaseUntil.IsZero() {
+ t.Fatalf("the ledger carries a bound (%v) before the seal's pass finished", l.LeaseUntil)
+ }
+ case "archive-sealed-snapshot":
+ // staged: the identity journalled, the sealed object in
+ // move-snapshots/ and the manifest not, the seal without files.
+ id := h.journalArchive(t)
+ snap := moves.SnapshotsPrefix(p1Host, p1ID)
+ _, sealedThere := h.f.buckets["data-ie"][snap+id.Name+".sealed"]
+ _, manifestThere := h.f.buckets["data-ie"][snap+id.Name+".json"]
+ if m.CutPhase != provision.CutStaged || !sealedThere || manifestThere || h.seal(t).HasFiles() {
+ t.Fatalf("durable state: %+v, sealed %v, manifest %v", m, sealedThere, manifestThere)
+ }
+ }
+ h.freshWorker()
+ h.pass(t)
+ m = h.move(t)
+ if m.State != provision.MoveLanding || m.CutPhase != provision.CutFlipped {
+ t.Fatalf("the replay: %+v", m)
+ }
+ keys, _ := h.s.List(ctx, moves.ReportPrefix("bab", "app", p1Host, p1ID))
+ n := 0
+ for _, k := range keys {
+ if strings.Contains(k, "stopped-") {
+ n++
+ }
+ }
+ seal := h.seal(t)
+ if n != 1 || !sealNamesArchive(seal, m.Source.Files.Sealed) || seal.N != m.Source.FinalTXID || !seal.ReachesN() {
+ t.Fatalf("%d reports; seal %+v; source %+v", n, seal, m.Source)
+ }
+ for _, o := range append(append(append([]moves.SealedObject{}, seal.Replica...), seal.Timers...), seal.Files...) {
+ if h.f.versionOf("data-uk", o.Key) != o.VersionID {
+ t.Fatalf("the seal names %s@%s, current %s", o.Key, o.VersionID, h.f.versionOf("data-uk", o.Key))
+ }
+ }
+ if l := h.ledger(t); !l.LeaseUntil.Equal(seal.LeaseUntil) {
+ t.Fatalf("ledger bound %v, seal bound %v", l.LeaseUntil, seal.LeaseUntil)
+ }
+ if c := h.claim(t); c.Op.Kind != hostclaim.KindLanding || c.Op.Exec != "uk-box" {
+ t.Fatalf("claim: %+v", c.Op)
+ }
+ })
+ }
+}