| index fe3b8a7d4..f647e9769 100644 |
| --- a/internal/host/movespaging_test.go |
| +++ b/internal/host/movespaging_test.go |
| @@ -671,3 +671,90 @@ func TestPagingIsRateLimited(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// outboxTree lists every file under dataDir's .moves directory, with its |
| +// bytes, so a test can tell whether a pass changed anything there. |
| +func outboxTree(t *testing.T, dataDir string) map[string]string { |
| + t.Helper() |
| + out := map[string]string{} |
| + root := filepath.Join(dataDir, ".moves") |
| + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { |
| + if errors.Is(err, fs.ErrNotExist) && p == root { |
| + return fs.SkipAll |
| + } |
| + if err != nil || d.IsDir() { |
| + return err |
| + } |
| + b, err := os.ReadFile(p) |
| + if err != nil { |
| + return err |
| + } |
| + rel, _ := filepath.Rel(root, p) |
| + out[filepath.ToSlash(rel)] = string(b) |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + return out |
| +} |
| + |
| +// Final review I1: with the switch off, a pass leaves both outboxes alone. |
| +// The pending page, the delivered page's tombstone and the kept log entry |
| +// stay on disk untouched, and nothing is sent or written, until the switch |
| +// is on again; then the same pass delivers them. |
| +func TestSwitchOffPassLeavesTheOutboxes(t *testing.T) { |
| + ctx := context.Background() |
| + dd := t.TempDir() |
| + now := time.Date(2026, 10, 11, 9, 0, 0, 0, time.UTC) |
| + var pages pagesSeen |
| + refuse := true |
| + var logged []moves.LogEvent |
| + logFails := true |
| + w := &movesWorker{cfg: moveCfg{DataDir: dd, Box: "ie-box", Enabled: true, Now: func() time.Time { return now }, Page: refusingPager(&pages, &refuse), |
| + LogEvent: func(_ context.Context, _ release.Store, _ provision.Record, ev moves.LogEvent) error { |
| + if logFails { |
| + return errors.New("the bucket is down") |
| + } |
| + logged = append(logged, ev) |
| + return nil |
| + }}, mirrors: map[string]lease.MirrorStore{}} |
| + rec := provision.Record{Account: "bab", App: "shop", Host: p1Host, Move: &provision.MoveState{ID: p1ID, State: provision.MoveStuck, From: "ie", To: "uk"}} |
| + // A page the pager refused: pending on disk. |
| + w.pageStuck(ctx, rec, "source", provision.StuckBaselineMismatch) |
| + // A delivered page of a move no record carries any more: a tombstone a |
| + // pass with the switch on would retire. |
| + refuse = false |
| + other := provision.Record{Host: p1Host, Move: &provision.MoveState{ID: "mv_zyxwvutsrqponmlkjihgfedcba"}} |
| + w.pageRolledBack(ctx, other) |
| + pages = nil |
| + // A log entry whose write failed: kept in the log outbox. |
| + w.observeMove(ctx, nil, provision.Record{Account: "bab", App: "shop", Host: p1Host, Region: "ie", |
| + Move: &provision.MoveState{ID: p1ID, State: provision.MovePreparing, From: "ie", To: "uk"}}) |
| + before := outboxTree(t, dd) |
| + if len(before) != 3 { |
| + t.Fatalf("the fixture's outboxes %v, want a pending page, a tombstone and a kept log entry", before) |
| + } |
| + logFails = false |
| + now = now.Add(time.Hour) // every backoff is over |
| + |
| + s := release.NewDirStore(t.TempDir()) |
| + w.cfg.Enabled = false |
| + if err := movesPass(ctx, s, w); err != nil { |
| + t.Fatal(err) |
| + } |
| + if len(pages) != 0 || len(logged) != 0 { |
| + t.Fatalf("a pass with the switch off sent pages %+v and wrote log entries %+v", pages, logged) |
| + } |
| + if after := outboxTree(t, dd); fmt.Sprint(after) != fmt.Sprint(before) { |
| + t.Fatalf("a pass with the switch off changed the outboxes:\nbefore %v\nafter %v", before, after) |
| + } |
| + |
| + w.cfg.Enabled = true |
| + if err := movesPass(ctx, s, w); err != nil { |
| + t.Fatal(err) |
| + } |
| + if n, _ := pagesWith(pages, "carlos move stuck: "); n != 1 || len(logged) != 1 || logged[0].Identity != "requested" { |
| + t.Fatalf("with the switch on again: pages %+v, log entries %+v", pages, logged) |
| + } |
| +} |