package host import ( "context" "encoding/json" "errors" "os" "os/exec" "path/filepath" "testing" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/release" ) func TestPlan14PrelaunchCancellationProcessDeath(t *testing.T) { if dir := os.Getenv("PLAN14_R11_RESERVE_CRASH"); dir != "" { f, disk, _ := generationLaunchFixture(t) if err := os.Mkdir(filepath.Join(dir, "pins"), 0700); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(dir, "fences"), 0700); err != nil { t.Fatal(err) } views, err := f.root.Guard.Root.GenerationInstallations(t.Context()) if err != nil || len(views) != 1 { t.Fatal(err) } in := views[0].Installation raw, _ := json.Marshal(map[string]any{"Schema": 1, "Installation": in, "State": map[string]any{"v": 1, "host": in.Host, "incarnation": in.Incarnation, "active_stamp": in.Stamp}}) if err = os.WriteFile(filepath.Join(dir, "fences", in.Host+".json"), raw, 0600); err != nil { t.Fatal(err) } launch, err := f.journal.Read(t.Context(), f.target) if err != nil { t.Fatal(err) } journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) if err != nil { t.Fatal(err) } if err = journal.Prelaunch(t.Context(), launch); err != nil { t.Fatal(err) } if err = journal.Close(); err != nil { t.Fatal(err) } disk.dir = filepath.Join(dir, "pins") disk.afterWrite = func(_ string, raw []byte) { var r release.GenerationReservation if json.Unmarshal(raw, &r) == nil && r.Revision == 1 { os.Exit(73) } } _, err = f.root.generations.reserve(t.Context(), f.root, f.peer, f.req) t.Fatal("reservation crash not reached", err) } dir := t.TempDir() cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=^TestPlan14PrelaunchCancellationProcessDeath$", "-test.count=1") cmd.Dir = dir cmd.Env = append(os.Environ(), "PLAN14_R11_RESERVE_CRASH="+dir, "TMPDIR="+dir) out, err := cmd.CombinedOutput() var status *exec.ExitError if !errors.As(err, &status) || status.ExitCode() != 73 { t.Fatal("crash child", err, string(out)) } disk := &generationDisk{rollbackDisk: &rollbackDisk{dir: filepath.Join(dir, "pins")}} r := readGenerationLaunch(t, disk) journal, err := hostbox.OpenLaunchJournal(filepath.Join(dir, "launch.db")) if err != nil { t.Fatal(err) } defer journal.Close() launches, err := journal.GenerationLaunches(t.Context()) if err != nil || len(launches) != 1 || len(launches[0].Reservations) != 0 { t.Fatal("pre-intention boundary", err, launches) } verifier := generationRetirement{guard: hostguard.Guard{DataDir: dir, LockDir: t.TempDir(), Root: hostguard.NewRootAuthority(&rollbackDisk{dir: filepath.Join(dir, "fences")})}, journal: journal, kernel: func(context.Context, release.GenerationReservation) (string, error) { return r.Binding.Digest, nil }} ledger := &release.RootGenerations{Store: disk} if err = verifier.retire(t.Context(), ledger, r); err != nil { t.Fatal(err) } if got := readGenerationLaunch(t, disk); got != r { t.Fatal("death substituted for writer closure") } if err = journal.CloseWriters(t.Context(), launches[0].Target); err != nil { t.Fatal(err) } if err = verifier.retire(t.Context(), ledger, r); err != nil { t.Fatal("cancellation recovery", err) } if got := readGenerationLaunch(t, disk); !got.Closed || got.Retirement == "" { t.Fatal("recovered reservation pinned") } }