//go:build linux && rootrunner package host import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostops" "github.com/carlosframework/platform/internal/rootparticipation" "github.com/carlosframework/platform/internal/rootstate" ) const migrationRebootDir = "/var/lib/plan14-p3-migration-reboot" func rebootMigration(t *testing.T) (rootMigration, *hostguard.RootAuthority) { t.Helper() open := func(name string) rootstate.Store { s, e := rootstate.Open(filepath.Join(migrationRebootDir, name), true, false) if e != nil { t.Fatal(e) } t.Cleanup(func() { s.Close() }) return s } state, execution, fences := open("migration"), open("execution"), open("fences") root := hostguard.NewRootAuthority(fences) m := rootMigration{prepareFloors: func(context.Context) error { return nil }, guardReady: func(context.Context) error { return nil }, state: state, execution: execution, identity: rootMigrationIdentity{"deploy", "box", "en", 1}, quiesce: func(context.Context) (func(), error) { return func() {}, nil }} m.observe = func(context.Context) (rootMigrationSnapshot, error) { raw, e := state.Read("snapshot.json") var s rootMigrationSnapshot if e == nil { e = json.Unmarshal(raw, &s) } return s, e } m.deny = func(ctx context.Context, item rootMigrationItem) error { return root.RetainMigrationDeny(ctx, item.Installation, item.Session) } m.recover = func(ctx context.Context, item rootMigrationItem) error { return verifyRootMigrationRetained(ctx, root, rootMigrationSnapshot{Items: []rootMigrationItem{item}}) } m.ready = func(ctx context.Context, s rootMigrationSnapshot) error { return verifyRootMigrationRetained(ctx, root, s) } m.publish = func(context.Context, hostops.CensusReport) error { return nil } return m, root } func TestRootRebootPlan14MigrationPhase1(t *testing.T) { if err := os.Mkdir(migrationRebootDir, 0700); err != nil { t.Fatal(err) } m, root := rebootMigration(t) _, s, _ := migrationFixture(t) item := rootMigrationItem{Installation: hostguard.RootInstallation{Host: "a.example", Incarnation: "inc", Stamp: 1, Account: "bab", App: "app", Record: "record", Box: "box", Enrollment: "en", Migration: 1, InventoryRevision: 1, Artifacts: hostguard.InstallationArtifacts{Schema: 1}}, Session: 9, ClaimETag: "claim"} s.Items = []rootMigrationItem{item} if err := m.write("snapshot.json", s); err != nil { t.Fatal(err) } // This phase-engine witness starts with a root-verified completed stop. The // separate historic runner exercises its signed import and physical census. if err := root.RetainMigrationDeny(t.Context(), item.Installation, item.Session); err != nil { t.Fatal(err) } target := hostguard.FenceTarget{Host: "a.example", Incarnation: "inc", Stamp: 1, Session: 9, Box: "box", Enrollment: "en", BootID: "old", ControllerID: "controller", ClaimETag: "claim", DirectoryRevision: "7", FenceID: "fence"} fences, err := rootstate.Open(filepath.Join(migrationRebootDir, "fences"), false, false) if err != nil { t.Fatal(err) } defer fences.Close() raw, err := fences.Read("a.example.json") if err != nil { t.Fatal(err) } var record map[string]any if json.Unmarshal(raw, &record) != nil { t.Fatal("record") } // Encode the typed completed receipt as pre-existing authority for this crash fixture. var state map[string]any encoded, _ := json.Marshal(record["State"]) if json.Unmarshal(encoded, &state) != nil { t.Fatal("state") } state["fence_journal"] = []hostguard.FenceReceipt{{Target: target, Bytes: []byte("root verified receipt"), EvidenceAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)}} record["State"] = state raw, _ = json.Marshal(record) if err := fences.Write("a.example.json", raw); err != nil { t.Fatal(err) } boot, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil { t.Fatal(err) } if err := m.state.Write("boot", boot); err != nil { t.Fatal(err) } cuts := 0 interrupted := errors.New("completion parent sync interrupted") m.state = rootstate.WithWriteFault(m.state, func(name, stage string, op func() error) error { if filepath.Base(name) == "migration.json" && stage == "parent-sync" { cuts++ if cuts == 4 { return interrupted } } return op() }) if err := m.run(t.Context()); !errors.Is(err, interrupted) { t.Fatal("crash boundary", cuts, err) } if _, err := (rootparticipation.Ledger{State: m.execution}).Census(); err == nil { t.Fatal("unsynced completion allowed grants") } } func TestRootRebootPlan14MigrationPhase2(t *testing.T) { if _, err := os.Stat(migrationRebootDir); err != nil { t.Fatal(err) } defer os.RemoveAll(migrationRebootDir) m, root := rebootMigration(t) before, err := m.state.Read("boot") if err != nil { t.Fatal(err) } now, err := os.ReadFile("/proc/sys/kernel/random/boot_id") if err != nil || strings.TrimSpace(string(before)) == strings.TrimSpace(string(now)) { t.Fatal("no real reboot", err) } if err := m.run(t.Context()); err != nil { t.Fatal("reboot resume", err) } if err := verifyRootMigrationState(t.Context(), m.state, m.execution, m.identity, root); err != nil { t.Fatal("startup verification", err) } v, err := root.View(t.Context(), "a.example", hostguard.Captured{Incarnation: "inc", Stamp: 1}) if err != nil || len(v.MigrationDenies) != 1 || v.MigrationDenies[0].Session != 9 || len(v.Receipts) != 1 || string(v.Receipts[0].Bytes) != "root verified receipt" { t.Fatal("reboot lost evidence", v, err) } if err := m.run(t.Context()); err != nil { t.Fatal("completed reboot retry", err) } }