carlos / platform Public

console: the witness that every claims-off console consumer ignores a fieldless record beside a retired slot

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 01d3acb678bf3a6b30973607824ed8f950a3a431 parent bb17122
1 file changed, +253 −0
  • internal/console/lifetimes_read_test.go +253 −0
diff --git a/internal/console/lifetimes_read_test.go b/internal/console/lifetimes_read_test.go
new file mode 100644
index 000000000..9a2c8f3e7
--- /dev/null
+++ b/internal/console/lifetimes_read_test.go
@@ -0,0 +1,253 @@
+package console
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/seeds"
+)
+
+// The console's consumers of instance records under the lifetime judge
+// (self-serve moves plan 1, Task 6): a request or worker iteration admitted
+// on judges every record it reads, the record then its incarnation slot. The
+// scene is the host witness's (TestHostConsumersIgnoreAFieldlessRecordBesideASlot,
+// internal/host/lifetime_gating_test.go): a fieldless record beside a slot
+// {1, retired}, an old console's late write after the lifetime ended. It
+// carries an offline move, a queued run and a seed a box could place, so
+// every consumer has something to act on. Each scene has an unjudged control
+// on a console still before the cutover.
+
+const (
+ staleHost = ltOld // the fieldless record behind its slot
+ validHost = ltOne // a legacy record with no slot: valid, the positive control
+)
+
+// staleScene writes the two records of shop and retires staleHost's
+// lifetime. Both start on fleet/acme, so a place to the shared fleet would
+// stamp each.
+func staleScene(t *testing.T, st release.Store) {
+ t.Helper()
+ ctx := context.Background()
+ now := time.Now().UTC()
+ seedID, err := seeds.NewID()
+ if err != nil {
+ t.Fatal(err)
+ }
+ stale := provision.Record{Schema: 1, Host: staleHost, Slug: "old", App: ltApp, Account: release.OpsAccount,
+ Channel: "edge", Region: "fleet/acme", CreatedAt: now,
+ Move: &provision.MoveState{ID: "mv_aaaaaaaaaaaaaaaaaaaaaaaaaa", Kind: provision.MoveKindOffline, From: "uk", To: "ie",
+ State: provision.OfflineStaging, RequestedBy: "alice@keymail.dev", RequestedAt: now, UpdatedAt: now},
+ ActiveRun: &provision.RunState{ID: "run_aaaaaaaaaaaaaaaaaaaaaaaaaa", State: provision.RunQueued, By: "alice@keymail.dev", Deadline: now.Add(time.Hour)},
+ Seed: &provision.SeedSource{ID: seedID, Kind: seeds.KindReplica, State: seeds.StateReady, SHA256: "a", Bytes: 2, By: "alice@keymail.dev", At: now},
+ }
+ valid := provision.Record{Schema: 1, Host: validHost, Slug: "one", App: ltApp, Account: release.OpsAccount,
+ Channel: "edge", Region: "fleet/acme", CreatedAt: now}
+ for _, rec := range []provision.Record{stale, valid} {
+ if err := provision.CreateRecord(ctx, st, rec); err != nil {
+ t.Fatal(err)
+ }
+ }
+ retiredSlot(t, st, staleHost)
+}
+
+func recordBytes(t *testing.T, st release.Store, host string) ([]byte, string) {
+ t.Helper()
+ b, etag, err := st.Get(context.Background(), provision.RecordKey(release.OpsAccount, ltApp, host))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return b, etag
+}
+
+func recordHostsOf(recs []provision.Record) string {
+ var out []string
+ for _, r := range recs {
+ out = append(out, r.Host)
+ }
+ return strings.Join(out, ",")
+}
+
+func TestEveryClaimsOffConsumerIgnoresAFieldlessRecordBesideASlot(t *testing.T) {
+ // on is a console with lifetimes on over the scene; before is the
+ // control, a console over the same scene before the cutover.
+ on := func(t *testing.T) ltOn {
+ l := newLtOn(t)
+ staleScene(t, l.fx.rel)
+ return l
+ }
+ before := func(t *testing.T) lifetimesFx {
+ l := newLifetimesFx(t)
+ staleScene(t, l.fx.rel)
+ return l
+ }
+ beforeCtx := func(t *testing.T, srv *Server) context.Context {
+ t.Helper()
+ u, ok, why := srv.lifetimesAdmit(context.Background(), false)
+ if !ok || u.mode != cutoverBefore {
+ t.Fatalf("admitted %v %q, want a unit before", ok, why)
+ }
+ t.Cleanup(u.done)
+ return context.WithValue(srv.recordCtx(context.Background(), u), admittedUnitKey{}, u)
+ }
+
+ t.Run("ReadRecordJudged and the console's readers", func(t *testing.T) {
+ l := on(t)
+ srv := l.fx.srv
+ if _, _, err := provision.ReadRecordJudged(context.Background(), srv.cfg.Releases, srv.judge, release.OpsAccount, ltApp, staleHost); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("ReadRecordJudged = %v, want not found", err)
+ }
+ ctx := onCtx(t, srv)
+ if provision.JudgeFrom(ctx) != srv.judge || srv.judge == nil {
+ t.Fatal("a unit admitted on carries no judge")
+ }
+ if _, _, err := srv.readRecord(ctx, release.OpsAccount, ltApp, staleHost); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("readRecord = %v, want not found", err)
+ }
+ if obs := srv.observeRecord(ctx, release.OpsAccount, ltApp, staleHost); !obs.Present || obs.Verdict != provision.LifetimeStale {
+ t.Fatalf("observeRecord = present %v, %v; want a present stale body", obs.Present, obs.Verdict)
+ }
+ if rec, _, err := srv.readRecord(ctx, release.OpsAccount, ltApp, validHost); err != nil || rec.Host != validHost {
+ t.Fatalf("readRecord of the valid record = %v", err)
+ }
+
+ b := before(t)
+ bctx := beforeCtx(t, b.fx.srv)
+ if provision.JudgeFrom(bctx) != nil {
+ t.Fatal("a unit admitted before carries a judge")
+ }
+ if rec, _, err := b.fx.srv.readRecord(bctx, release.OpsAccount, ltApp, staleHost); err != nil || rec.ActiveRun == nil || !rec.Move.Offline() || rec.Seed == nil {
+ t.Fatalf("fixture: unjudged, the stale record does not read: %v", err)
+ }
+ })
+
+ t.Run("the five judged listings", func(t *testing.T) {
+ l := on(t)
+ srv := l.fx.srv
+ ctx := onCtx(t, srv)
+ recs, _, err := srv.listRecords(ctx, release.OpsAccount, ltApp, instanceListMax)
+ if err != nil || recordHostsOf(recs) != validHost {
+ t.Errorf("listRecords = %s, %v", recordHostsOf(recs), err)
+ }
+ recs, truncated, skipped, err := srv.listRecordsComplete(ctx, release.OpsAccount, ltApp, instanceListMax)
+ if err != nil || truncated || len(skipped) != 0 || recordHostsOf(recs) != validHost {
+ t.Errorf("listRecordsComplete = %s, %v %v %v", recordHostsOf(recs), truncated, skipped, err)
+ }
+ recs, unreadable, err := srv.listRecordsStrict(ctx, release.OpsAccount, ltApp, instanceListMax)
+ if err != nil || len(unreadable) != 0 || recordHostsOf(recs) != validHost {
+ t.Errorf("listRecordsStrict = %s, %v %v", recordHostsOf(recs), unreadable, err)
+ }
+ recs, unreadable, err = srv.listAllRecordsStrict(ctx)
+ if err != nil || len(unreadable) != 0 || recordHostsOf(recs) != validHost {
+ t.Errorf("listAllRecordsStrict = %s, %v %v", recordHostsOf(recs), unreadable, err)
+ }
+ recs, complete, err := srv.listAllRecordsTolerant(ctx)
+ if err != nil || !complete || recordHostsOf(recs) != validHost {
+ t.Errorf("listAllRecordsTolerant = %s, %v %v", recordHostsOf(recs), complete, err)
+ }
+ // The judged replica listing the instance API offers likewise.
+ if recs, err := provision.ListRecordReplicasJudged(ctx, srv.cfg.Releases, provision.JudgeFrom(ctx), release.OpsAccount, ltApp, instanceListMax); err != nil || len(recs) != 0 {
+ t.Errorf("ListRecordReplicasJudged = %s, %v", recordHostsOf(recs), err)
+ }
+ requireListing(t, l.lifetimesFx, validHost)
+
+ // Control: before the cutover every listing has both.
+ b := before(t)
+ bctx := beforeCtx(t, b.fx.srv)
+ want := staleHost + "," + validHost
+ if got, _, err := b.fx.srv.listRecords(bctx, release.OpsAccount, ltApp, instanceListMax); err != nil || recordHostsOf(got) != want {
+ t.Errorf("fixture: unjudged listRecords = %s, %v", recordHostsOf(got), err)
+ }
+ if got, _, err := b.fx.srv.listAllRecordsStrict(bctx); err != nil || recordHostsOf(got) != want {
+ t.Errorf("fixture: unjudged listAllRecordsStrict = %s, %v", recordHostsOf(got), err)
+ }
+ requireListing(t, b, staleHost, validHost)
+ })
+
+ t.Run("the console GET", func(t *testing.T) {
+ l := on(t)
+ base := instancesURL(l.fx, release.OpsAccount, ltApp)
+ if code, _, body := rawReq(t, http.MethodGet, base+"/"+staleHost, l.member, ""); code != http.StatusNotFound {
+ t.Fatalf("GET of the stale record = %d %s, want 404", code, body)
+ }
+ if code, _, body := rawReq(t, http.MethodGet, base+"/"+validHost, l.member, ""); code != http.StatusOK {
+ t.Fatalf("GET of the valid record = %d %s", code, body)
+ }
+ b := before(t)
+ if code, _, body := rawReq(t, http.MethodGet, instancesURL(b.fx, release.OpsAccount, ltApp)+"/"+staleHost, b.member, ""); code != http.StatusOK {
+ t.Fatalf("fixture: unjudged GET = %d %s", code, body)
+ }
+ })
+
+ t.Run("the browser placement handler and placeAppTarget", func(t *testing.T) {
+ for _, door := range []string{"browser", "placeAppTarget"} {
+ t.Run(door, func(t *testing.T) {
+ l := on(t)
+ staleRaw, _ := recordBytes(t, l.fx.rel, staleHost)
+ switch door {
+ case "browser":
+ c := signInPaul(t, l.fx)
+ csrf := sessionToken(t, l.fx, c)
+ code, loc, body := postForm(t, c, l.fx.u("/apps/"+ltApp+"/place"), url.Values{"csrf": {csrf}, "target": {""}})
+ if code != http.StatusSeeOther || !strings.HasSuffix(loc, "/apps/"+ltApp+"/instances") {
+ t.Fatalf("place = %d -> %q %s", code, loc, body)
+ }
+ case "placeAppTarget":
+ acct, err := l.fx.st.AccountBySqid(release.OpsAccount)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if st, msg := l.fx.srv.placeAppTarget(onCtx(t, l.fx.srv), acct, ltApp, ""); st != 0 {
+ t.Fatalf("placeAppTarget = %d %q", st, msg)
+ }
+ }
+ if got, _ := recordBytes(t, l.fx.rel, staleHost); !bytes.Equal(got, staleRaw) {
+ t.Fatalf("the place rewrote the stale record:\n%s\nwas\n%s", got, staleRaw)
+ }
+ // The positive control: the place ran and stamped the valid one.
+ rec, err := provision.GetRecord(context.Background(), l.fx.rel, release.OpsAccount, ltApp, validHost)
+ if err != nil || rec.Region != "" {
+ t.Fatalf("the valid record was not placed: %+v, %v", rec, err)
+ }
+ })
+ }
+ // Control: before the cutover the place reads the stale record, and
+ // its unplaced seed refuses the whole place.
+ b := before(t)
+ acct, err := b.fx.st.AccountBySqid(release.OpsAccount)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if st, msg := b.fx.srv.placeAppTarget(beforeCtx(t, b.fx.srv), acct, ltApp, ""); st != http.StatusConflict {
+ t.Fatalf("fixture: unjudged, the place answered %d %q, want the seed's refusal", st, msg)
+ }
+ })
+
+ t.Run("claimRawUpdateRecord under a judged context", func(t *testing.T) {
+ l := on(t)
+ raw, etag := recordBytes(t, l.fx.rel, staleHost)
+ rec, err := provision.DecodeRecord(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ rec.Gate = "maintenance"
+ if err := provision.UpdateRecord(onCtx(t, l.fx.srv), l.fx.srv.cfg.Releases, rec, etag); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("UpdateRecord of the stale record = %v, want not found", err)
+ }
+ if got, _ := recordBytes(t, l.fx.rel, staleHost); !bytes.Equal(got, raw) {
+ t.Fatal("the update rewrote the stale record")
+ }
+ // Control: unjudged, the same update lands.
+ if err := provision.UpdateRecord(context.Background(), l.fx.rel, rec, etag); err != nil {
+ t.Fatalf("fixture: unjudged, the update = %v", err)
+ }
+ })
+}