host: the two-box offline move through a lifetime carries the pair on every row it holds and opens
TestOfflineMoveE2E gains three traces with the judge on and the record created through its lifetime: the done move holds the source at (1, 1), lands the target at (1, 1), opens it at (1, 2) and keeps (1, 2) through finish; an abort from landed reopens the source at (1, 2), removes the target's row and keeps (1, 2) through finish; a record moved on before the target's release leaves the row held. move_seq joins the record diff's allowlist.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+126
−3
internal/host/movesoffline_e2e_test.go+126 −3
diff --git a/internal/host/movesoffline_e2e_test.go b/internal/host/movesoffline_e2e_test.go| index c93c1ae79..193eca8b3 100644 |
| --- a/internal/host/movesoffline_e2e_test.go |
| +++ b/internal/host/movesoffline_e2e_test.go |
| @@ -27,6 +27,7 @@ import ( |
| "github.com/carlosframework/platform/internal/console" |
| "github.com/carlosframework/platform/internal/domains" |
| "github.com/carlosframework/platform/internal/hostops" |
| + "github.com/carlosframework/platform/internal/hostwrite" |
| "github.com/carlosframework/platform/internal/instancekey" |
| "github.com/carlosframework/platform/internal/layout" |
| "github.com/carlosframework/platform/internal/lease" |
| @@ -302,7 +303,7 @@ func newE2EWorld(t *testing.T, opts landOpts) *e2eWorld { |
| // The deployment: the ops account, app's manifest and the instance's |
| // record at home. |
| wd := &offWorld{t: t, s: rs, acct: release.OpsAccount, now: now} |
| - if opts.judged { |
| + if opts.judged || opts.lifetime { |
| wd.judge = provision.NewLifetimeJudge() |
| } |
| if _, _, err := rs.Get(ctx, domains.AccountManifestKey(wd.acct)); errors.Is(err, os.ErrNotExist) { |
| @@ -314,7 +315,11 @@ func newE2EWorld(t *testing.T, opts landOpts) *e2eWorld { |
| if err := provision.PutManifest(ctx, rs, wd.acct, "app", provision.Manifest{Schema: 1, App: "app", Domain: "example.com"}); err != nil { |
| t.Fatal(err) |
| } |
| - wd.create(offHost) |
| + if opts.lifetime { |
| + wd.createLifetime(offHost, 1) |
| + } else { |
| + wd.create(offHost) |
| + } |
| // The boxes. ie-box is the home's one box; uk-box is uk's. |
| sw := &sealWorld{offWorld: wd, fake: fake, keyB64: keyB64, priv: priv} |
| @@ -545,7 +550,7 @@ func asJSONMap(t *testing.T, raw []byte) map[string]any { |
| func (w *e2eWorld) requireRecordDiff(when string) map[string]any { |
| w.t.Helper() |
| after := asJSONMap(w.t, w.recordRaw()) |
| - allowed := map[string]bool{"region": true, "move": true, "last_move": true, "record_seq": true} |
| + allowed := map[string]bool{"region": true, "move": true, "last_move": true, "record_seq": true, "move_seq": true} |
| keys := map[string]bool{} |
| for k := range w.recBefore { |
| keys[k] = true |
| @@ -1074,6 +1079,13 @@ func TestOfflineMoveE2E(t *testing.T) { |
| t.Run("nested source with launch files", func(t *testing.T) { offlineMoveDone(t, landOpts{nested: true}) }) |
| t.Run("no files directory", func(t *testing.T) { offlineMoveDone(t, landOpts{noFiles: true}) }) |
| t.Run("rollback after a tampered archive", offlineMoveRollback) |
| + // Self-serve moves plan 1 Task 7: the records are created through |
| + // their lifetime with the judge on, and every row the move holds or |
| + // opens carries (incarnation, serving_seq) from the transition that |
| + // produced it. |
| + t.Run("done, through a lifetime", func(t *testing.T) { offlineMoveDone(t, landOpts{lifetime: true}) }) |
| + t.Run("aborted from landed, through a lifetime", offlineMoveAbortedFromLanded) |
| + t.Run("a release skips a record that moved on", offlineReleaseSkipsAMovedRecord) |
| // Harness Run 9, finding 7: the doors driven with nothing between them |
| // but what production runs (each door's serial bump, the boxes' own |
| // nudges and the offline re-assert loop), first back to back, then with |
| @@ -1264,13 +1276,23 @@ func offlineMoveDone(t *testing.T, opts landOpts) { |
| } |
| w.begin() |
| + if opts.lifetime { |
| + w.requirePair(w.ie, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after start") |
| + } |
| w.seal() |
| w.flipAndLand() |
| + if opts.lifetime { |
| + w.requirePair(w.uk, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after the landing") |
| + } |
| if r := w.wd.reportFor(w.id, moves.OfflineRoleTarget, "uk-box"); !r.Items["env-pin"].OK { |
| t.Fatalf("env-pin across the two agent environments: %+v", r.Items["env-pin"]) |
| } |
| w.requireStopped("after the landing") |
| w.open() |
| + if opts.lifetime { |
| + w.requirePair(w.uk, registry.Pair{Incarnation: 1, ServingSeq: 2}, "", "after open") |
| + w.requirePair(w.ie, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after open") |
| + } |
| w.requireTarget() |
| retired := w.requireRetired() |
| @@ -1296,6 +1318,13 @@ func offlineMoveDone(t *testing.T, opts landOpts) { |
| t.Fatalf("after finish: ie lists %v, uk lists %v", w.ie.lists(offHost), w.uk.lists(offHost)) |
| } |
| w.requireRetiredAfterFinish(retired) |
| + if opts.lifetime { |
| + // The target's row keeps open's sequence through finish. |
| + w.requirePair(w.uk, registry.Pair{Incarnation: 1, ServingSeq: 2}, "", "after finish") |
| + if rec := w.wd.record(); rec.Incarnation != 1 || rec.MoveSeq != 3 { |
| + t.Fatalf("after finish the record is at (%d, %d), want (1, 3)", rec.Incarnation, rec.MoveSeq) |
| + } |
| + } |
| after := w.requireRecordDiff("after finish") |
| if after["region"] != "uk" { |
| @@ -1526,3 +1555,97 @@ func (w *e2eWorld) parkedTarget() map[string]bool { |
| } |
| return out |
| } |
| + |
| +// createLifetime creates host's record through its lifetime, in the create |
| +// protocol's order: the host incarnation slot at inc, live, then the record |
| +// carrying incarnation inc. |
| +func (wd *offWorld) createLifetime(host string, inc uint64) { |
| + wd.t.Helper() |
| + ctx := context.Background() |
| + if _, err := provision.WriteHostIncarnation(ctx, wd.s, host, |
| + provision.HostIncarnation{Account: wd.acct, App: "app", Incarnation: inc, State: provision.SlotLive, Intent: "create-e2e"}, ""); err != nil { |
| + wd.t.Fatal(err) |
| + } |
| + rec := provision.Record{Schema: 1, Host: host, Slug: strings.SplitN(host, ".", 2)[0], Account: wd.acct, App: "app", Region: "", Channel: "stable", CreatedAt: wd.now, Incarnation: inc} |
| + if err := provision.CreateRecord(ctx, wd.s, rec); err != nil { |
| + wd.t.Fatal(err) |
| + } |
| +} |
| + |
| +// requirePair: b's row for the host is at want, held by op ("" is ready). |
| +func (w *e2eWorld) requirePair(b *offBox, want registry.Pair, op, when string) { |
| + w.t.Helper() |
| + rt := b.row(offHost) |
| + if rt == nil { |
| + w.t.Fatalf("%s: %s has no row", when, b.name) |
| + } |
| + got := registry.Pair{Incarnation: rt.Incarnation, ServingSeq: rt.ServingSeq} |
| + held := op != "" |
| + if got != want || rt.HoldOp != op || registry.IsHold(rt.Status) != held || (!held && rt.Status != "ready") { |
| + w.t.Fatalf("%s: %s's row is %s %q at %+v, want %q at %+v", when, b.name, rt.Status, rt.HoldOp, got, op, want) |
| + } |
| +} |
| + |
| +// An abort from landed: the source reopens at the abort's sequence, the |
| +// target's row is gone, and finish leaves the source's row where the abort |
| +// put it. |
| +func offlineMoveAbortedFromLanded(t *testing.T) { |
| + w := newE2EWorld(t, landOpts{lifetime: true}) |
| + w.begin() |
| + w.requirePair(w.ie, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after start") |
| + w.seal() |
| + w.flipAndLand() |
| + if st := w.wd.record().Move.State; st != provision.OfflineLanded { |
| + t.Fatalf("fixture: state %s, want landed", st) |
| + } |
| + w.requirePair(w.uk, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after the landing") |
| + |
| + w.accept("abort", "", provision.OfflineReverting) |
| + w.until("the revert", nextIs("finish")) |
| + w.requirePair(w.ie, registry.Pair{Incarnation: 1, ServingSeq: 2}, "", "after the revert") |
| + w.finish("aborted") |
| + w.requireRecordDiff("after the abort") |
| + w.tickAll() |
| + w.requirePair(w.ie, registry.Pair{Incarnation: 1, ServingSeq: 2}, "", "after finish") |
| + if rt := w.uk.row(offHost); rt != nil { |
| + t.Fatalf("uk-box has a row after the abort: %+v", rt) |
| + } |
| + if !w.ie.lists(offHost) || w.uk.lists(offHost) { |
| + t.Fatalf("after the abort: ie lists %v, uk lists %v", w.ie.lists(offHost), w.uk.lists(offHost)) |
| + } |
| + if rec := w.wd.record(); rec.MoveSeq != 3 || rec.Move != nil { |
| + t.Fatalf("after finish the record is at move_seq %d, move %+v", rec.MoveSeq, rec.Move) |
| + } |
| +} |
| + |
| +// The target's open releases from the record it observes under the locks, |
| +// through armOffline's releaser: a record that moved on between the pass's |
| +// listing and the release is not released for, and the row stays held. |
| +func offlineReleaseSkipsAMovedRecord(t *testing.T) { |
| + w := newE2EWorld(t, landOpts{lifetime: true}) |
| + probe := moveCfg{} |
| + armOffline(&probe, t.TempDir(), w.uk.reg, nil, nil, hostwrite.Config{}, w.uk.getenv) |
| + if probe.ReleaseHoldSeq == nil { |
| + t.Fatal("armOffline wires no ReleaseHoldSeq") |
| + } |
| + w.begin() |
| + w.seal() |
| + w.flipAndLand() |
| + w.accept("open", "", provision.OfflineOpen) |
| + moved := 0 |
| + w.uk.w.cfg.beforeOfflineRelease = func(host string) { |
| + if moved > 0 { |
| + return |
| + } |
| + moved++ |
| + w.wd.move(provision.OfflineFinishingTransition(w.id)) |
| + } |
| + w.tick(w.uk) |
| + if moved != 1 { |
| + t.Fatal("fixture: the target's open never reached its release") |
| + } |
| + w.requirePair(w.uk, registry.Pair{Incarnation: 1, ServingSeq: 1}, w.id, "after a release over a moved record") |
| + if st := w.wd.record().Move.State; st != provision.OfflineFinishing { |
| + t.Fatalf("fixture: state %s", st) |
| + } |
| +} |