console: tests for fix round 1
The switch-off operator sentinel with pinned instances; the prepare before the cutover is not enabled; freshness and the lifetime before the walk's verdicts; the run before an unreadable journal; the unreadable journal's 502; a cancel of a returning move; TestOperatorStatusReadsNoIdentity (an operator status poll of a staging self-serve move and of an operator move reads nothing under control/boxes/ and not the roster).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+93
−4
internal/console/selfserve_door_test.go+93 −4
diff --git a/internal/console/selfserve_door_test.go b/internal/console/selfserve_door_test.go| index b4eb2f097..3bb14d047 100644 |
| --- a/internal/console/selfserve_door_test.go |
| +++ b/internal/console/selfserve_door_test.go |
| @@ -14,6 +14,7 @@ import ( |
| "github.com/carlosframework/platform/internal/boxid" |
| "github.com/carlosframework/platform/internal/boxid/boxidtest" |
| + "github.com/carlosframework/platform/internal/hostactors" |
| "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -213,7 +214,7 @@ func (w *ssWorld) refusedSS(body string, wantCode int, wantMsg string) { |
| w.t.Fatalf("a refusal changed %s", k) |
| } |
| } |
| - if open, _ := w.rows(); open != nil && wantMsg != "a move to uk is already preparing" { |
| + if open, _, _ := w.fx.st.SelfServeMoveForHost("bab", "shop", offlineHost); open != nil && wantMsg != "a move to uk is already preparing" { |
| w.t.Fatalf("a refusal admitted a row: %+v", open) |
| } |
| } |
| @@ -426,6 +427,20 @@ func TestParticipantPinning(t *testing.T) { |
| t.Fatalf("an operator door read identity records: %v", r) |
| } |
| }) |
| + t.Run("operator, the switch off: the instances pinned, the sentinel reserved", func(t *testing.T) { |
| + w := newSelfServeWorld(t, false) |
| + w.fx.srv.cfg.SelfServeMoves = "" |
| + sel, ref, _ := pin(t, w, false) |
| + if ref != nil { |
| + t.Fatal(ref.msg) |
| + } |
| + if sel.Source[0].InstanceID != "i-ie-box" || sel.Target.InstanceID != "i-uk-box" || !sel.Sentinel { |
| + t.Fatalf("source %+v target %+v sentinel %v", sel.Source[0], sel.Target, sel.Sentinel) |
| + } |
| + if got := sel.reservationKeys(); len(got) != 1 || got[0] != moves.OfflineSentinel { |
| + t.Fatalf("an operator selection with the switch off reserves %v", got) |
| + } |
| + }) |
| for _, c := range []struct { |
| name string |
| edit func(b *status.Box) |
| @@ -619,6 +634,27 @@ func TestPrepareRefusesBeforeWriting(t *testing.T) { |
| {name: "a replica listing from the future", setup: func(w *ssWorld) { |
| w.facts(func(f *status.HostFacts) { f.ReplicaCollectedAt = time.Now().Add(2 * time.Minute).Unix() }) |
| }, body: ssBody, code: 409, msg: "the source box has not reported recently. try again in a minute"}, |
| + // Freshness and the lifetime come before the walk's own verdicts. |
| + {name: "over the cap, collected long ago", setup: func(w *ssWorld) { |
| + w.facts(func(f *status.HostFacts) { f.OverCap, f.CollectedAt = true, time.Now().Add(-11*time.Minute).Unix() }) |
| + }, body: ssBody, code: 409, msg: "the source box has not reported recently. try again in a minute"}, |
| + {name: "not known, of another lifetime", setup: func(w *ssWorld) { |
| + w.facts(func(f *status.HostFacts) { f.Known = false; f.Incarnation++ }) |
| + }, body: ssBody, code: 409, msg: "the source box has not reported recently. try again in a minute"}, |
| + // The record's checks come before the journal's read. |
| + {name: "an open run with the rows unreadable", setup: func(w *ssWorld) { |
| + rawEdit(w, func(m map[string]any) { |
| + m["run"] = map[string]any{"id": "run-1", "state": provision.RunQueued, "deadline": time.Now().Add(time.Hour)} |
| + }) |
| + if _, err := w.fx.st.db.Exec(`ALTER TABLE self_serve_moves RENAME TO self_serve_moves_gone`); err != nil { |
| + w.t.Fatal(err) |
| + } |
| + }, body: ssBody, code: 409, msg: (&provision.MoveRunOpenError{Run: provision.RunState{ID: "run-1"}}).Sentence()}, |
| + {name: "the rows unreadable", setup: func(w *ssWorld) { |
| + if _, err := w.fx.st.db.Exec(`ALTER TABLE self_serve_moves RENAME TO self_serve_moves_gone`); err != nil { |
| + w.t.Fatal(err) |
| + } |
| + }, body: ssBody, code: 502, msg: "could not read the move"}, |
| {name: "a truncated walk", setup: func(w *ssWorld) { w.facts(func(f *status.HostFacts) { f.Truncated = true }) }, |
| body: ssBody, code: 409, msg: "could not measure the account's files. try again in a minute"}, |
| {name: "no lane", setup: func(w *ssWorld) { |
| @@ -735,7 +771,7 @@ func TestPrepareMigratesALegacyRecordBoundThenAdmits(t *testing.T) { |
| t.Fatalf("the moved record was migrated to %d", rec.Incarnation) |
| } |
| }) |
| - t.Run("before the cutover a legacy record is settling", func(t *testing.T) { |
| + t.Run("before the cutover the prepare is not enabled", func(t *testing.T) { |
| w := newOfflineWorld(t, oneEach...) |
| w.fx.srv.cfg.SelfServeMoves = "offline" |
| owner, err := w.fx.st.NewAppToken(OpsAccountID, "shop", "paul@keymail.dev", "shop-mover", true, time.Now()) |
| @@ -752,8 +788,11 @@ func TestPrepareMigratesALegacyRecordBoundThenAdmits(t *testing.T) { |
| } |
| } |
| code, _, out := rawReq(t, http.MethodPost, strings.TrimSuffix(w.base, "/offline"), owner, ssBody) |
| - if code != http.StatusConflict || errorText(out) != msgLifetimeSettling { |
| - t.Fatalf("prepare before the cutover = %d %s", code, out) |
| + if code != http.StatusConflict || errorText(out) != "self-serve moves are not enabled on this console" { |
| + t.Fatalf("prepare before the cutover = %d %s, want 409 not enabled", code, out) |
| + } |
| + if open, _, _ := w.fx.st.SelfServeMoveForHost("bab", "shop", offlineHost); open != nil { |
| + t.Fatalf("a row was admitted before the cutover: %+v", open) |
| } |
| }) |
| } |
| @@ -906,6 +945,13 @@ func TestMutationsNameTheMoveAndRace(t *testing.T) { |
| resume() |
| want(t, "the cancel after the finish", code, out, http.StatusConflict, "no move is in flight") |
| }) |
| + t.Run("a cancel of a move the job is returning", func(t *testing.T) { |
| + w := newSelfServeWorld(t, false) |
| + row := w.prepared() |
| + w.setPhase(row.ID, ssReverting) |
| + code, out := cancel(w, row.ID) |
| + want(t, "cancel while reverting", code, out, http.StatusConflict, "the move was confirmed and cannot be cancelled. it completes or returns on its own") |
| + }) |
| t.Run("a stale nonce cannot confirm or consume", func(t *testing.T) { |
| w := newSelfServeWorld(t, false) |
| row := w.prepared() |
| @@ -1148,3 +1194,46 @@ func TestSwitchOffIsByteIdentical(t *testing.T) { |
| t.Fatal("a legacy door admitted a row") |
| } |
| } |
| + |
| +// TestOperatorStatusReadsNoIdentity: the operator's status poll judges a |
| +// staging self-serve move's start without the identity reads, which are the |
| +// self-serve door's and the job's; an operator move's poll reads none |
| +// either. |
| +func TestOperatorStatusReadsNoIdentity(t *testing.T) { |
| + poll := func(t *testing.T, w *ssWorld) { |
| + t.Helper() |
| + for _, b := range []string{"ie-box", "uk-box"} { |
| + region := "eu-west-1" |
| + if b == "uk-box" { |
| + region = "eu-west-2" |
| + } |
| + _, raw := w.signer.Record(t, b, moveTestAccount, region, "i-"+b) |
| + offlinePut(t, w.fx.rel, boxid.Key(b), raw) |
| + } |
| + kr := &keyReads{Store: w.fx.rel} |
| + w.fx.srv.cfg.Releases = kr |
| + defer func() { w.fx.srv.cfg.Releases = w.fx.rel }() |
| + v := w.status() |
| + if v.State != provision.OfflineStaging { |
| + t.Fatalf("status = %+v", v) |
| + } |
| + if r := append(kr.read(boxid.Prefix), kr.read(hostactors.Key)...); len(r) != 0 { |
| + t.Fatalf("an operator status poll read %v", r) |
| + } |
| + if len(kr.read(provision.RecordKey("bab", "shop", offlineHost))) == 0 { |
| + t.Fatal("the poll read nothing: the counter sees no reads") |
| + } |
| + } |
| + t.Run("a self-serve move in staging", func(t *testing.T) { |
| + w := newSelfServeWorld(t, false) |
| + row := w.prepared() |
| + w.queued(row) |
| + w.edit(provision.OfflineQueuedToStaging(row.ID, time.Now(), provision.Receipt{})) |
| + poll(t, w) |
| + }) |
| + t.Run("an operator move in staging", func(t *testing.T) { |
| + w := newSelfServeWorld(t, false) |
| + w.ok("/stage", ssBody, provision.OfflineStaging) |
| + poll(t, w) |
| + }) |
| +} |