package provision import ( "context" "encoding/json" "errors" "strings" "testing" "time" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/seeds" ) func runTestRecord(t *testing.T, s release.Store) Record { t.Helper() rec := Record{Schema: 1, Host: "a.go.example.com", Slug: "s", App: "app", Account: "bab", Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC()} if err := CreateRecord(context.Background(), s, rec); err != nil { t.Fatal(err) } return rec } func setRun(t *testing.T, s release.Store, rec Record, mut func(*Record) error) (*Record, error) { t.Helper() return SetRecordRun(context.Background(), s, rec.Account, rec.App, rec.Host, mut) } func TestRunAdmitPickFinishClear(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now().Add(time.Minute) got, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", By: "alice@keymail.dev", Deadline: at})) if err != nil || got.ActiveRun.State != RunQueued || got.ActiveRun.By != "alice@keymail.dev" || !got.ActiveRun.Open() { t.Fatalf("admit: %+v, %v", got, err) } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_b", Deadline: at})); !errors.Is(err, ErrRunOpen) { t.Fatalf("second admit: %v, want ErrRunOpen", err) } var open *RunOpenError if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_b", Deadline: at})); !errors.As(err, &open) || open.Run.ID != "run_a" || open.Run.By != "alice@keymail.dev" { t.Fatalf("the refusal must name the open run and its caller: %v", err) } if got, err = setRun(t, s, rec, RunPick("run_a", "box-1", "boot-1", at.Add(time.Hour))); err != nil || got.ActiveRun.State != RunPicked || got.ActiveRun.Box != "box-1" { t.Fatalf("pick: %+v, %v", got, err) } if _, err := setRun(t, s, rec, RunPick("run_a", "box-2", "boot-2", at)); !errors.Is(err, ErrRunConflict) { t.Fatalf("a second pick: %v, want ErrRunConflict", err) } if _, err := setRun(t, s, rec, RunClear("run_a")); !errors.Is(err, ErrRunConflict) { t.Fatalf("clearing an open run: %v, want ErrRunConflict", err) } if got, err = setRun(t, s, rec, RunFinish("run_a")); err != nil || got.ActiveRun.State != RunDone || got.ActiveRun.Open() { t.Fatalf("finish: %+v, %v", got, err) } if got, err = setRun(t, s, rec, RunClear("run_a")); err != nil || got.ActiveRun != nil { t.Fatalf("clear: %+v, %v", got, err) } } func TestRunCancelBeforeAndAfterPickup(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now().Add(time.Minute) if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); err != nil { t.Fatal(err) } got, err := setRun(t, s, rec, RunCancelTransition("run_a")) if err != nil || got.ActiveRun.State != RunCancelled || got.ActiveRun.Open() { t.Fatalf("queued cancel: %+v, %v", got, err) } if _, err := setRun(t, s, rec, RunPick("run_a", "box-1", "boot-1", at)); !errors.Is(err, ErrRunConflict) { t.Fatal("a cancelled run was picked (codex R2-6)") } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_b", Deadline: at})); err != nil { t.Fatalf("a terminal run must not block the next: %v", err) } if _, err := setRun(t, s, rec, RunPick("run_b", "box-1", "boot-1", at)); err != nil { t.Fatal(err) } if got, err = setRun(t, s, rec, RunCancelTransition("run_b")); err != nil || got.ActiveRun.State != RunCancel || !got.ActiveRun.Open() { t.Fatalf("picked cancel: %+v, %v", got, err) } if got, err = setRun(t, s, rec, RunCancelTransition("run_b")); err != nil || got.ActiveRun.State != RunCancel { t.Fatalf("a second cancel is idempotent: %+v, %v", got, err) } if got, err = setRun(t, s, rec, RunFinish("run_b")); err != nil || got.ActiveRun.State != RunDone { t.Fatalf("finish after cancel: %+v, %v", got, err) } } func TestRunExpireOnlyFromQueued(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now() if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); err != nil { t.Fatal(err) } if got, err := setRun(t, s, rec, RunExpire("run_a")); err != nil || got.ActiveRun.State != RunExpired { t.Fatalf("expire: %+v, %v", got, err) } if _, err := setRun(t, s, rec, RunPick("run_a", "late-box", "boot", at)); !errors.Is(err, ErrRunConflict) { t.Fatal("an expired run was picked by a late box (codex R2-6)") } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_b", Deadline: at})); err != nil { t.Fatal(err) } if _, err := setRun(t, s, rec, RunPick("run_b", "box-1", "boot-1", at)); err != nil { t.Fatal(err) } if _, err := setRun(t, s, rec, RunExpire("run_b")); !errors.Is(err, ErrRunConflict) { t.Fatal("a picked run was expired: the late pickup must win") } } func TestRunAdmitRefusesAMoveAndAnUncommittedImport(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now() if _, err := SetRecordMove(context.Background(), s, rec.Account, rec.App, rec.Host, MoveRequest("uk", at)); err != nil { t.Fatal(err) } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); !errors.Is(err, ErrRunMoving) { t.Fatalf("admit while moving: %v, want ErrRunMoving", err) } // An import walks its path by the seed transitions; admission is tried // at each state on the way. s2 := release.NewDirStore(t.TempDir()) seedID, err := seeds.NewID() if err != nil { t.Fatal(err) } imp := Record{Schema: 1, Host: "a.go.example.com", Slug: "s", App: "app", Account: "bab", Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC(), Seed: &SeedSource{ID: seedID, Kind: seeds.KindImport, State: seeds.StateAwaitingUpload}} if err := CreateRecord(context.Background(), s2, imp); err != nil { t.Fatal(err) } for _, step := range []struct { mut func(*Record) error state string refused bool }{ {SeedUploaded(seedID), seeds.StateUploaded, true}, {SeedStopped(seedID), seeds.StateReady, true}, {SeedSwapped(seedID), seeds.StateSwapped, true}, {SeedPlace(seedID), seeds.StatePlaced, true}, {SeedCommit(seedID), seeds.StateCommitted, false}, } { if _, err := SetRecordSeed(context.Background(), s2, imp.Account, imp.App, imp.Host, step.mut); err != nil { t.Fatalf("to %s: %v", step.state, err) } _, err := setRun(t, s2, imp, RunAdmit(RunAdmission{ID: "run_" + step.state, Deadline: at})) if step.refused != errors.Is(err, ErrRunImport) { t.Errorf("import %s: err %v, refused want %v (codex R2-2)", step.state, err, step.refused) } } } // A move request is refused while a run is open, in both move transitions, // and the refusal is a move conflict whose text is RUN-C43 (codex 1, R1-1). func TestMoveTransitionsRefuseWhileARunIsOpen(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: time.Now()})); err != nil { t.Fatal(err) } inc := strings.Repeat("a", 64) for name, mut := range map[string]func(*Record) error{ "legacy": MoveRequest("uk", time.Now()), "phase 1": MoveStartTransition(MoveStart{ID: "mv_" + strings.Repeat("a", 26), Incarnation: inc, From: "ie", To: "uk", Cut: "auto", Now: time.Now()}), } { _, err := SetRecordMove(context.Background(), s, rec.Account, rec.App, rec.Host, mut) var mro *MoveRunOpenError if !errors.Is(err, ErrMoveConflict) || !errors.As(err, &mro) || !strings.Contains(err.Error(), "a run is open on this instance: run_a") { t.Errorf("%s: %v", name, err) } } if _, err := setRun(t, s, rec, RunExpire("run_a")); err != nil { t.Fatal(err) } if _, err := SetRecordMove(context.Background(), s, rec.Account, rec.App, rec.Host, MoveRequest("uk", time.Now())); err != nil { t.Fatalf("a terminal run must not block a move: %v", err) } } // Ordinary record writers carry the run through unchanged, as they carry // Maintenance (run spec ยง5.1). func TestOrdinaryWritersCarryActiveRun(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC) if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", By: "alice@keymail.dev", Deadline: at})); err != nil { t.Fatal(err) } runJSON := func() string { raw, _, err := s.Get(context.Background(), RecordKey(rec.Account, rec.App, rec.Host)) if err != nil { t.Fatal(err) } var m map[string]json.RawMessage if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } return string(m["run"]) } want := runJSON() if !strings.Contains(want, `"state":"queued"`) { t.Fatalf("run = %s", want) } if err := SetRecordRegion(context.Background(), s, rec.Account, rec.App, rec.Host, "uk"); err != nil { t.Fatal(err) } if got := runJSON(); got != want { t.Fatalf("SetRecordRegion: run %s, want %s", got, want) } cur, etag, err := GetRecordETag(context.Background(), s, rec.Account, rec.App, rec.Host) if err != nil { t.Fatal(err) } cur.Channel = "beta" if err := UpdateRecord(context.Background(), s, *cur, etag); err != nil { t.Fatal(err) } if got := runJSON(); got != want { t.Fatalf("UpdateRecord: run %s, want %s", got, want) } } // A terminal run's id is not admitted again, whatever its state: a box // retrying RunPick for it must not find it queued (codex R2-6). func TestRunAdmitRefusesAReusedID(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now().Add(time.Minute) if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); err != nil { t.Fatal(err) } if _, err := setRun(t, s, rec, RunCancelTransition("run_a")); err != nil { t.Fatal(err) } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); !errors.Is(err, ErrRunConflict) { t.Fatalf("re-admitting a cancelled run's id: %v, want ErrRunConflict", err) } got, _, err := GetRecordETag(context.Background(), s, rec.Account, rec.App, rec.Host) if err != nil || got.ActiveRun.State != RunCancelled { t.Fatalf("the cancelled run must be untouched: %+v, %v", got.ActiveRun, err) } if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_b", Deadline: at})); err != nil { t.Fatalf("a new id replaces the terminal run: %v", err) } } func TestRunPickRefusesMalformedInput(t *testing.T) { s := release.NewDirStore(t.TempDir()) rec := runTestRecord(t, s) at := time.Now().Add(time.Minute) if _, err := setRun(t, s, rec, RunAdmit(RunAdmission{ID: "run_a", Deadline: at})); err != nil { t.Fatal(err) } for name, mut := range map[string]func(*Record) error{ "no box": RunPick("run_a", "", "boot-1", at), "no boot": RunPick("run_a", "box-1", "", at), "no deadline": RunPick("run_a", "box-1", "boot-1", time.Time{}), } { if _, err := setRun(t, s, rec, mut); !errors.Is(err, ErrRunConflict) { t.Errorf("%s: %v, want ErrRunConflict", name, err) } } got, _, err := GetRecordETag(context.Background(), s, rec.Account, rec.App, rec.Host) if err != nil || got.ActiveRun.State != RunQueued { t.Fatalf("a refused pick must leave the run queued: %+v, %v", got.ActiveRun, err) } }