//go:build linux package host import ( "context" "errors" "testing" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" ) // N2 (roster binding, task 3b). A serving park's reboot preparation persists // the actor it was prepared under and its commit compares that record with a // fresh one. Root's inventory actor here is the production identity over a // signed roster, and every read of it follows another console create's hold // churn (a real Reserve and ReleaseFinishedHold), so the preparation's reread, // its persisted copy and the commit all see a different roster revision. They // still match: the preparation stores the bound actor. A quarantine and a // readmission between prepare and commit change the binding and are refused // (the quarantine by Verify, the readmission by the installation's stamp). func TestServingRebootPreparationSurvivesHoldChurn(t *testing.T) { for _, step := range []string{"hold", "hold-root-restart", "quarantine-readmit"} { t.Run(step, func(t *testing.T) { ctx := t.Context() roster := newBindingRosterFixture(t, "box-1", "ie") identity := roster.identity() actor, err := identity.Verify(ctx) if err != nil { t.Fatal(err) } if !hostactors.ValidBinding(actor.Binding) || actor.Binding != roster.binding() { t.Fatalf("production actor binding %q", actor.Binding) } f, r := servingRebootPreparationFixtureFor(t, "parked", actor) churning := func(identity hostbox.IdentityProvider) func(context.Context) (hostbox.Actor, error) { return func(ctx context.Context) (hostbox.Actor, error) { roster.holdChurn() return identity.Verify(ctx) } } f.p.root.InventoryActor = churning(identity) id := r.Authorization.ID before := roster.read().Roster.Revision if err := f.p.prepareServingReboot(ctx, id); err != nil { t.Fatal("prepare under hold churn:", err) } if after := roster.read().Roster.Revision; after < before+4 { t.Fatalf("prepare read the actor without churn: revision %d -> %d", before, after) } body, err := f.disk.Read(servingRebootPreparationName(id, servingOtherBoot)) if err != nil { t.Fatal(err) } var prep servingParkRebootPreparation if servingParkDecode(body, &prep) != nil || prep.Active == nil || prep.Active.Actor != actor.Bound() || prep.Active.Actor.Revision != 0 { t.Fatalf("persisted preparation actor: %+v", prep.Active) } roster.holdChurn() p := f.p switch step { case "hold-root-restart": // A restarted root: a new ledger and handler over the same // private state, with a newly built identity. ledger := *f.p.ledger root := f.p.root root.InventoryActor = churning(roster.identity()) p = &servingParkRoot{root: root, ledger: &ledger, empty: f.p.empty} case "quarantine-readmit": roster.quarantine("loss_1") if err := p.commitServingReboot(ctx, id); err == nil { t.Fatal("commit accepted while quarantined") } roster.readmit() if b := roster.binding(); b == actor.Binding { t.Fatal("readmission kept the binding") } } err = p.commitServingReboot(ctx, id) if step == "quarantine-readmit" { // The installation's stamp is the old binding, so the fresh // installed-state check (claimservingparksource_linux.go, the // inventory against actor.Binding) refuses before the persisted // preparation is compared: ErrTypedInventory, not ErrChanged. if !errors.Is(err, hostbox.ErrTypedInventory) { t.Fatalf("commit after readmission: %v, want ErrTypedInventory", err) } if _, err := f.disk.Read("current-namespace.json"); err == nil { t.Fatal("refused commit bound the namespace") } return } if err != nil { t.Fatal("commit after hold churn:", err) } fs := p.ledger.fs.(*servingParkFS) if err := fs.recoverRecordedNamespace(ctx); err != nil { t.Fatal(err) } current, err := p.ledger.read(id) if err != nil || current.Source.BootID != servingOtherBoot { t.Fatal("committed authority", err) } }) } }