//go:build linux package host import ( "context" "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/park" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // These tests drive claim Plan 9 D4 on a landing claim written only by // main's own mutators: the create, the source's writer, BeginMove, the seal, // the source's evidence, the flip, the landing phases, moves' local // inventory under the flip's stamp F, and the probe's AdmitWriter. No // park-local chore exists at any point. The wake's production Stop, Admit, // Prepare and Restore closures run against the real guard, launch journal, // root stop coordinator and park.Local; the kernel is recoveryKernel, a // fixture that reads the real launch journal. var ( plan9D4Probe = activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second, Probe: true} plan9D4Wake = activator.StartPolicy{MaxStarts: 5, Window: 30 * time.Second} ) type plan9D4 struct { f *recoveryFixture data, db string inc string flip uint64 permissive bool crossBox int handovers []hostguard.FenceTarget } func (d *plan9D4) local() park.Local { return park.Local{Guard: d.f.guard, Claims: d.f.claims, DataDir: d.data, Box: "box-1"} } func (d *plan9D4) step(t *testing.T, what string, fn func(*hostclaim.Claim) error) { t.Helper() if _, err := hostclaim.Update(t.Context(), d.f.claims, d.f.read(), fn); err != nil { t.Fatalf("%s: %v", what, err) } } // plan9D4Landing moves the host from box-0 to box-1 with main's mutators and // leaves the landing at probing, box-1's local install tagged F. func plan9D4Landing(t *testing.T) *plan9D4 { t.Helper() ctx := t.Context() f := newRecoveryFixture(t) st := release.NewDirStore(t.TempDir()) f.claims, f.guard.Claims = st, hostguard.StoreReader{Store: st} d := &plan9D4{f: f, data: f.guard.DataDir, db: provision.DefaultDBPath(f.guard.DataDir, recoveryHost)} if err := os.Mkdir(filepath.Join(d.data, ".parked"), 0770); err != nil { t.Fatal(err) } in := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + recoveryHost + ".json", Account: "bab", App: "jam", Host: recoveryHost, Region: "ie", RecordRegion: "ie", CreatedAt: f.now} body := []byte(`{"schema":1,"host":"shop.bab.oncarlos.com","account":"bab","app":"jam","region":"ie","created_at":"2026-10-06T12:00:00Z"}`) d.step(t, "create", hostclaim.BeginCreate(in, body, "create", "console", f.now)) d.step(t, "complete the create", hostclaim.CompleteCreate("create", "box-0")) h := f.read().Claim.Holder d.step(t, "admit the source's writer", hostclaim.AdmitWriter(hostclaim.Admission{ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "box-0", Mode: "cold", NewGeneration: "src", KeepStamp: h.Stamp > h.Session, Region: "ie"})) d.step(t, "begin the move", hostclaim.BeginMove("m1", "console", "box-0", "box-1", "ie", f.now)) d.step(t, "touch", hostclaim.Touch("m1", []string{"ie"}, []string{"box-1"})) d.step(t, "seal", hostclaim.SetMovePhase("m1", hostclaim.KindMoving, hostclaim.PhaseMoving, hostclaim.PhaseSealed)) d.step(t, "source evidence", hostclaim.ExecToNone("m1", "box-0", "moves/bab/jam/"+recoveryHost+"/m1/stopped-source.json", hostclaim.JournalEvidenceSource)) h = f.read().Claim.Holder d.step(t, "flip", hostclaim.Flip("m1", "box-1", h.Stamp, h.Generation)) if err := f.guard.Activate(ctx, recoveryHost, "box-1", nil); err != nil { t.Fatal(err) } h = f.read().Claim.Holder d.inc, d.flip = h.Incarnation, h.Stamp order := provision.LandingOrder() for i := 0; order[i] != provision.LandProbing; i++ { if order[i] == provision.LandLocal { d.installLocal(t) } d.step(t, "landing "+order[i+1], hostclaim.SetMovePhase("m1", hostclaim.KindLanding, order[i], order[i+1])) } return d } // installLocal is what moves' local phase leaves: the database and the // marker naming the prepared generation (the op id), the files directory and // .env, all four recorded under F through park.Local. func (d *plan9D4) installLocal(t *testing.T) { t.Helper() files := filepath.Join(d.data, recoveryHost) if err := os.Mkdir(files, 0700); err != nil { t.Fatal(err) } for path, body := range map[string]string{d.db: "landing database", filepath.Join(files, "index.html"): "files", filepath.Join(d.data, recoveryHost+".env"): "A=1\n"} { if err := os.WriteFile(path, []byte(body), 0600); err != nil { t.Fatal(err) } } if err := hostbox.WriteGenerationMarker(d.db+".gen", "m1"); err != nil { t.Fatal(err) } check := hostclaim.RequireMoveOp("m1", hostclaim.KindLanding, provision.LandLocal) paths := []string{recoveryHost + ".db", recoveryHost + ".db.gen", recoveryHost, recoveryHost + ".env"} if err := d.local().RecordInventory(t.Context(), recoveryHost, hostguard.Captured{Incarnation: d.inc, Stamp: d.flip}, "flat", paths, check); err != nil { t.Fatal(err) } } // plan9D4Inputs is the moves Inputs owner's landing admission: the first // probe opens the prepared generation and keeps F; every later writer takes // a fresh generation and its own stamp (claim ยง6.4a items 3 and 4). type plan9D4Inputs struct { recoveryInputs d *plan9D4 } func (in plan9D4Inputs) Select(_ context.Context, _ registry.Route, o hostclaim.Observed, _ activator.StartPolicy) (ClaimLaunchInputs, error) { h, op := o.Claim.Holder, o.Claim.Op generation, coverage := fmt.Sprintf("g%d", o.Claim.Gen+1), "" if h.WriterBox != "box-1" { generation, coverage = op.ID, "sealed" } return ClaimLaunchInputs{Roles: []string{"main"}, Identity: hostwrite.Identity{Config: "config", Release: "release"}, Admission: hostclaim.Admission{ObservedOpID: op.ID, ObservedPhase: op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "box-1", Mode: "cold", NewGeneration: generation, KeepStamp: h.Stamp > h.Session, Coverage: coverage, Region: "ie", PredecessorRegion: "ie"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), DBPath: in.d.db, Empty: true, ReadyPath: "/ready"}, nil } // handover stands in for production's AfterAdmission dispatch // (claimAdmissionHandover). A first probe's predecessor is the source on // box-0: D2's cross-box handover, tested in plan9_d2_test.go. Every later // predecessor is box-1's own writer, and coldAdmissionHook first takes the // predecessor session's completed receipt exactly as here, then asks root's // ColdHandover to retire it, which this unprivileged fixture cannot run. func (d *plan9D4) handover(ctx context.Context, prior, _ hostclaim.Observed) error { old := prior.Claim.Holder if old.WriterBox != "box-1" { d.crossBox++ return nil } if d.permissive { return nil } receipt, err := d.f.guard.CompletedColdPredecessor(ctx, prior.Host, hostguard.Captured{Incarnation: old.Incarnation, Stamp: old.Stamp}, old.Session, prior.Claim.Op.ID, prior.Claim.Op.Phase) if err != nil { return err } if receipt.Target.ObligationID != hostguard.WriterStopObligation(old.Session) || receipt.Target.Box != "box-1" { return hostbox.ErrChanged } d.handovers = append(d.handovers, receipt.Target) return verifyRetirementReceipt(ctx, receipt) } // controller starts one agent process on box-1. bound is the moves C1 // integration, bound only with CARLOS_HOST_CLAIM and CARLOS_MOVES_PHASE1 on. func (d *plan9D4) controller(boot, controller string, pid int, start string, bound bool) *recoveryController { c := d.f.controller(boot, controller, pid, start) c.wake.Inputs = plan9D4Inputs{recoveryInputs{d.f}, d} c.wake.AfterAdmission = d.handover if bound { c.wake.Continue = hostclaim.SessionContinues c.wake.Operations = &plan9D2Attempts{} c.wake.ProbeReady = func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil } c.wake.RetireArtifacts = claimArtifactRetirement{Local: d.local(), Continue: hostclaim.SessionContinues}.Retire c.wake.RecoverArtifacts = claimArtifactRetirement{Local: d.local(), Continue: hostclaim.SessionContinues}.Recover } return c } // start runs one landing writer start as the wake does: the production Stop // of the captured predecessor (Prerequisites' call), a stand-in for the S3 // lease, then Admit, Prepare and the production Restore closure, whose // ColdRestore calls RetireArtifacts. The written database stands in for the // started app's own writes. func (d *plan9D4) start(t *testing.T, c *recoveryController, p activator.StartPolicy, stop bool, materialize ...func(hostclaim.Observed, activator.ClaimStart) error) (hostclaim.Observed, error) { t.Helper() ctx := t.Context() o := d.f.read() if stop { if err := c.wake.Stop(ctx, o); err != nil { t.Fatalf("stop the predecessor session %d: %v", o.Claim.Holder.Session, err) } } live, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) done := make(chan struct{}) go func() { <-live.Done(); close(done) }() c.wake.mu.Lock() c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done} c.wake.mu.Unlock() admitted, err := c.runtime.Admit(ctx, registry.Route{Host: recoveryHost}, o, p) if err != nil { t.Fatalf("admit: %v", err) } s, err := c.runtime.Prepare(ctx, registry.Route{Host: recoveryHost}, admitted, p) if err != nil { t.Fatalf("prepare: %v", err) } if err := s.Restore(ctx); err != nil { return admitted, err } for _, run := range materialize { if err := run(admitted, s); err != nil { return admitted, err } } session := admitted.Claim.Holder.Session return admitted, os.WriteFile(d.db, []byte(fmt.Sprintf("written by session %d", session)), 0600) } func (d *plan9D4) mustStart(t *testing.T, c *recoveryController, p activator.StartPolicy, stop bool) hostclaim.Observed { t.Helper() admitted, err := d.start(t, c, p, stop) if err != nil { t.Fatalf("restore: %v", err) } return admitted } func (d *plan9D4) read(t *testing.T, path string) string { t.Helper() body, err := os.ReadFile(path) if err != nil { t.Fatal(err) } return string(body) } func (d *plan9D4) parked(t *testing.T, retirement string) string { return filepath.Join(d.data, ".parked", d.inc, recoveryHost, retirement, "artifacts") } // requireRetired checks one restart or cold wake: the predecessor's writer // stop, its database and marker parked under this admission's retirement and // tagged with its stamp, the files and environment left at F, no chore. func (d *plan9D4) requireRetired(t *testing.T, prior, admitted hostclaim.Observed, recovered bool) { t.Helper() old, h := prior.Claim.Holder, admitted.Claim.Holder req := d.f.requests[len(d.f.requests)-1] if req.ObligationID != hostguard.WriterStopObligation(old.Session) || req.OperationID != "m1" || req.Phase != prior.Claim.Op.Phase || req.WriterSession != old.Session || req.InstallationStamp != old.Stamp || req.Box != "box-1" || req.Recover != recovered { t.Fatalf("the predecessor's stop request: %+v", req) } if n := len(d.handovers); n == 0 || d.handovers[n-1].Session != old.Session || d.handovers[n-1].Stamp != old.Stamp { t.Fatalf("the cold handover did not find session %d's receipt: %+v", old.Session, d.handovers) } retired := d.parked(t, fmt.Sprintf("superseded-%d-%d", h.Session, old.Stamp)) if got := d.read(t, filepath.Join(retired, recoveryHost+".db.gen")); got != old.Generation+"\n" { t.Fatalf("parked marker %q, want %s", got, old.Generation) } if got := d.read(t, filepath.Join(retired, recoveryHost+".db")); got != fmt.Sprintf("written by session %d", old.Session) { t.Fatalf("parked database %q", got) } if got := d.read(t, d.db+".gen"); got != h.Generation+"\n" { t.Fatalf("live marker %q, want %s", got, h.Generation) } c := d.f.read().Claim paths := c.Inventory["box-1"].Paths if paths[recoveryHost+".db"] != h.Stamp || paths[recoveryHost+".db.gen"] != h.Stamp || paths[recoveryHost] != d.flip || paths[recoveryHost+".env"] != d.flip || len(paths) != 4 { t.Fatalf("inventory after retiring stamp %d: %+v", old.Stamp, paths) } if len(c.Obligations) != 0 { t.Fatalf("a chore appeared: %+v", c.Obligations) } if d.read(t, filepath.Join(d.data, recoveryHost, "index.html")) != "files" || d.read(t, filepath.Join(d.data, recoveryHost+".env")) != "A=1\n" { t.Fatal("the landing's files or environment moved") } } func TestPlan9D4RestartsAndColdWakeRetireOnlyTheSupersededGeneration(t *testing.T) { d := plan9D4Landing(t) ctx := t.Context() c := d.controller("boot-1", "controller-1", 4101, "start-1", true) // The first probe opens the prepared generation and keeps F: nothing is // superseded and nothing is retired. Its predecessor is the source. first := d.mustStart(t, c, plan9D4Probe, false) if h := first.Claim.Holder; h.Generation != "m1" || h.Stamp != d.flip || d.crossBox != 1 || len(d.f.requests) != 0 { t.Fatalf("first probe: %+v crossBox=%d requests=%d", h, d.crossBox, len(d.f.requests)) } if entries, err := os.ReadDir(filepath.Join(d.data, ".parked")); err != nil || len(entries) != 0 { t.Fatalf("the first probe parked something: %v %v", entries, err) } // First restart: stop the probe as the landing's own writer, take a // fresh generation and stamp, retire the prepared database tagged F. second := d.mustStart(t, c, plan9D4Probe, true) d.requireRetired(t, first, second, false) // Second restart: the same authority again, now for the restarted // probe's session and its stamp. Nothing was consumed by the first. third := d.mustStart(t, c, plan9D4Probe, true) d.requireRetired(t, second, third, false) // The probe passes; the commit keeps the writer session (D1). d.step(t, "probed", hostclaim.KeepWriterSession("m1", hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbing, hostclaim.PhaseLandingProbed))) d.step(t, "committed", hostclaim.KeepWriterSession("m1", hostclaim.SetMovePhase("m1", hostclaim.KindLanding, hostclaim.PhaseLandingProbed, hostclaim.PhaseLandingCommitted))) committed := d.f.read() // The agent restarts. Its cold wake after the open stops the committed // probe session in recovery, under the earlier controller's tuple. after := d.controller("boot-1", "controller-2", 4102, "start-2", true) wake := d.mustStart(t, after, plan9D4Wake, true) d.requireRetired(t, committed, wake, true) if wake.Claim.Op.Phase != hostclaim.PhaseLandingCommitted || wake.Claim.Holder.Session <= committed.Claim.Holder.Session { t.Fatalf("cold wake: %+v", wake.Claim) } // Every predecessor holds root's completed full stop of its own session. for _, prior := range []hostclaim.Observed{first, second, committed} { h := prior.Claim.Holder receipts, err := d.f.guard.CompletedWriterStops(ctx, recoveryHost, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, "m1", "box-1") if err != nil || len(receipts) != 1 || receipts[0].Target.Session != h.Session || verifyRetirementReceipt(ctx, receipts[0]) != nil { t.Fatalf("session %d's receipt: %+v %v", h.Session, receipts, err) } } } func TestPlan9D4WriterStopIsTheLandingsOwnWriterOnly(t *testing.T) { d := plan9D4Landing(t) ctx := t.Context() c := d.controller("boot-1", "controller-1", 4101, "start-1", true) // Before the first probe the writer is the source's session on box-0. // The writer stop does not apply; main's park-local refusal stands and // root is never asked. if err := c.wake.Stop(ctx, d.f.read()); !errors.Is(err, hostbox.ErrChanged) || len(d.f.requests) != 0 { t.Fatalf("stop of the source's session from the target: %v requests=%d", err, len(d.f.requests)) } request := func() StopRequest { o := d.f.read() h := o.Claim.Holder return StopRequest{Region: "ie", Host: recoveryHost, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, WriterSession: h.Session, OperationID: "m1", Phase: o.Claim.Op.Phase, ObligationID: hostguard.WriterStopObligation(h.Session), Box: "box-1", Enrollment: "enrollment-1", BootID: "boot-1", ControllerID: "controller-1", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7", RosterRevision: 7} } if _, err := d.f.root(recoveryStopDrain{}).FenceAndStop(ctx, request()); !errors.Is(err, ErrClaimStopIdentity) { t.Fatalf("root stopped the source's session as box-1's writer: %v", err) } d.mustStart(t, c, plan9D4Probe, false) // With either flag off (no Continue), the local landing writer keeps // main's refusal: no root request, no fence. c.wake.Continue = nil if err := c.wake.Stop(ctx, d.f.read()); !errors.Is(err, hostbox.ErrChanged) || len(d.f.requests) != 0 { t.Fatalf("flag-off stop: %v requests=%d", err, len(d.f.requests)) } c.wake.Continue = hostclaim.SessionContinues // Root refuses a writer-session stop for any other session or region. for name, mutate := range map[string]func(*StopRequest){ "another session's label": func(r *StopRequest) { r.ObligationID = hostguard.WriterStopObligation(r.WriterSession + 1) }, "another region": func(r *StopRequest) { r.Region = "uk" }, } { r := request() mutate(&r) if _, err := d.f.root(recoveryStopDrain{}).FenceAndStop(ctx, r); !errors.Is(err, ErrClaimStopIdentity) { t.Fatalf("%s: %v", name, err) } } // A chore bearing the writer-session name is never taken for the writer: // main falls back to the chore, and the shared authority check refuses // the name before root is asked; root refuses it too. h := d.f.read().Claim.Holder label := hostguard.WriterStopObligation(h.Session) d.step(t, "a chore with the writer's name", hostclaim.QueueLocalPark("m1", "box-1", func() string { return label })) if err := c.wake.Stop(ctx, d.f.read()); !errors.Is(err, hostbox.ErrChanged) || len(d.f.requests) != 0 { t.Fatalf("stop beside a chore named %s: %v requests=%d", label, err, len(d.f.requests)) } if _, err := d.f.root(recoveryStopDrain{}).FenceAndStop(ctx, request()); !errors.Is(err, ErrClaimStopIdentity) { t.Fatalf("root stopped beside a chore named %s: %v", label, err) } receipts, err := d.f.guard.CompletedWriterStops(ctx, recoveryHost, hostguard.Captured{Incarnation: h.Incarnation, Stamp: h.Stamp}, "m1", "box-1") if err != nil || len(receipts) != 0 { t.Fatalf("a refused stop left a receipt: %+v %v", receipts, err) } } func TestPlan9D4RetirementRequiresTheWriterStopReceipt(t *testing.T) { for name, collect := range map[string]func(*testing.T, *plan9D4, *recoveryController){ // The predecessor was never stopped. "no receipt": func(*testing.T, *plan9D4, *recoveryController) {}, // Root stopped the predecessor under another obligation name: that // receipt is evidence for that name, not the writer's. "another scope": func(t *testing.T, d *plan9D4, c *recoveryController) { o := d.f.read() h := o.Claim.Holder drain, err := c.wake.Producers(t.Context(), o) if err != nil { t.Fatal(err) } r := StopRequest{Region: "ie", Host: recoveryHost, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, WriterSession: h.Session, OperationID: "m1", Phase: o.Claim.Op.Phase, ObligationID: "probe-chore", Box: "box-1", Enrollment: "enrollment-1", BootID: "boot-1", ControllerID: "controller-1", ExpectedClaimETag: o.ETag, ExpectedDirectoryRevision: "7", RosterRevision: 7} if _, err := d.f.root(drain).FenceAndStop(t.Context(), r); err != nil { t.Fatal(err) } }, } { t.Run(name, func(t *testing.T) { d := plan9D4Landing(t) c := d.controller("boot-1", "controller-1", 4101, "start-1", true) d.mustStart(t, c, plan9D4Probe, false) collect(t, d, c) d.permissive = true if _, err := d.start(t, c, plan9D4Probe, false); !errors.Is(err, hostguard.ErrNoFenceReceipt) { t.Fatalf("retired without the writer's receipt: %v", err) } if got := d.read(t, d.db+".gen"); got != "m1\n" { t.Fatalf("marker %q", got) } if entries, err := os.ReadDir(filepath.Join(d.data, ".parked")); err != nil || len(entries) != 0 { t.Fatalf("parked without the writer's receipt: %v %v", entries, err) } if paths := d.f.read().Claim.Inventory["box-1"].Paths; paths[recoveryHost+".db.gen"] != d.flip || paths[recoveryHost+".db"] != d.flip { t.Fatalf("inventory changed: %+v", paths) } }) } } func TestPlan9D4RetirementRefusesOutsideTheLandingContract(t *testing.T) { // A restarted probe admitted after the production stop, with Restore not // yet run: the hook is called directly with each refused shape. setup := func(t *testing.T) (*plan9D4, *recoveryController, hostclaim.Observed, claimArtifactRetirement) { d := plan9D4Landing(t) c := d.controller("boot-1", "controller-1", 4101, "start-1", true) d.mustStart(t, c, plan9D4Probe, false) if err := c.wake.Stop(t.Context(), d.f.read()); err != nil { t.Fatal(err) } o := d.f.read() in, err := c.wake.Inputs.Select(t.Context(), registry.Route{Host: recoveryHost}, o, plan9D4Probe) if err != nil { t.Fatal(err) } admitted, err := hostclaim.Update(t.Context(), d.f.claims, o, hostclaim.AdmitWriter(in.Admission)) if err != nil { t.Fatal(err) } if err := d.f.guard.Activate(t.Context(), recoveryHost, "box-1", nil); err != nil { t.Fatal(err) } return d, c, admitted, claimArtifactRetirement{Local: d.local(), Continue: hostclaim.SessionContinues} } untouched := func(t *testing.T, d *plan9D4, name string) { t.Helper() if entries, err := os.ReadDir(filepath.Join(d.data, ".parked")); err != nil || len(entries) != 0 { t.Fatalf("%s parked: %v %v", name, entries, err) } if got := d.read(t, d.db+".gen"); got != "m1\n" { t.Fatalf("%s moved the marker: %q", name, got) } } for name, tc := range map[string]struct { retire func(claimArtifactRetirement) claimArtifactRetirement admitted func(hostclaim.Observed) hostclaim.Observed generation string mutate func(*testing.T, *plan9D4, *recoveryController) }{ "flag off": {retire: func(r claimArtifactRetirement) claimArtifactRetirement { r.Continue = nil; return r }}, "serving": {admitted: func(o hostclaim.Observed) hostclaim.Observed { c := *o.Claim c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "box-1"} o.Claim = &c return o }}, "foreign": {generation: "stranger"}, "current": {generation: "admitted"}, "untagged": {mutate: func(t *testing.T, d *plan9D4, _ *recoveryController) { d.retag(t, recoveryHost+".db.gen", 0) }}, "split": {mutate: func(t *testing.T, d *plan9D4, _ *recoveryController) { d.retag(t, recoveryHost+".db", d.flip+1) }}, "successor": {mutate: func(t *testing.T, d *plan9D4, c *recoveryController) { d.readmit(t, c) }}, "other op": {admitted: func(o hostclaim.Observed) hostclaim.Observed { c := *o.Claim; c.Op.ID = "m0"; o.Claim = &c; return o }}, "other exec": {admitted: func(o hostclaim.Observed) hostclaim.Observed { c := *o.Claim c.Op.Exec = "box-0" o.Claim = &c return o }}, } { t.Run(name, func(t *testing.T) { d, c, admitted, r := setup(t) if tc.retire != nil { r = tc.retire(r) } if tc.mutate != nil { tc.mutate(t, d, c) } if tc.admitted != nil { admitted = tc.admitted(admitted) } generation := "m1" switch tc.generation { case "admitted": generation = admitted.Claim.Holder.Generation case "": default: generation = tc.generation } if err := r.Retire(t.Context(), admitted, ClaimLaunchInputs{DBPath: d.db}, generation); !errors.Is(err, hostbox.ErrChanged) { t.Fatalf("%s: %v", name, err) } untouched(t, d, name) }) } // The same admission retires once its shape is right. d, _, admitted, r := setup(t) if err := r.Retire(t.Context(), admitted, ClaimLaunchInputs{DBPath: d.db}, "m1"); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(d.parked(t, fmt.Sprintf("superseded-%d-%d", admitted.Claim.Holder.Session, d.flip)), recoveryHost+".db.gen")); err != nil { t.Fatal(err) } } // retag rewrites one inventory tag of box-1 (0 removes it). func (d *plan9D4) retag(t *testing.T, path string, stamp uint64) { t.Helper() d.step(t, "retag "+path, func(c *hostclaim.Claim) error { entry := c.Inventory["box-1"] if stamp == 0 { delete(entry.Paths, path) } else { entry.Paths[path] = stamp } c.Inventory["box-1"] = entry return nil }) } // readmit admits yet another writer, so the admission the hook was called // for is no longer the claim's. func (d *plan9D4) readmit(t *testing.T, c *recoveryController) { t.Helper() o := d.f.read() in, err := c.wake.Inputs.Select(t.Context(), registry.Route{Host: recoveryHost}, o, plan9D4Probe) if err != nil { t.Fatal(err) } d.step(t, "readmit", hostclaim.AdmitWriter(in.Admission)) }