moves-phase1-6 Task 3 final fix (2): the purge's Resume drops its hosts' move log rows
TestPurgeResumeDropsItsHostsMoveLogRows runs a real purge through Resume from the moment its journal stands at retired, with one retired host recorded in it (the fixture cannot purge an instance's host without the boxes), and checks that host's incarnation's rows go and another incarnation's stay. Removing the purge call from Resume fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 file changed,
+54
−0
internal/console/movelog_retire_test.go+54 −0
diff --git a/internal/console/movelog_retire_test.go b/internal/console/movelog_retire_test.go| index 3ebe550c1..f87185495 100644 |
| --- a/internal/console/movelog_retire_test.go |
| +++ b/internal/console/movelog_retire_test.go |
| @@ -4,6 +4,7 @@ import ( |
| "context" |
| "encoding/json" |
| "net/http" |
| + "net/url" |
| "testing" |
| "time" |
| @@ -129,3 +130,56 @@ func TestPurgingAnAppPurgesItsInstancesMoveLogRows(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// Final fix, ruling 2: the purge's own run (Resume) drops the rows of the |
| +// incarnations its journal records, from a retired journal on. The world's |
| +// app has no instance the fixture can purge (an instance's host purge needs |
| +// the boxes), so the journal gains one, already retired, the moment it is |
| +// "retired"; Resume then runs from there to done. |
| +func TestPurgeResumeDropsItsHostsMoveLogRows(t *testing.T) { |
| + cx := newClaimCaseWith(t) |
| + cx.purgeWorld(t) |
| + cx.quiesce() |
| + queueTestMoveLog(t, cx.st, "gone.example.com", "mv_1", "done", "inc-gone") |
| + queueTestMoveLog(t, cx.st, "gone.example.com", "mv_2", "done", "inc-earlier") |
| + v := url.Values{"csrf": {cx.csrf}, "app": {"titogo"}, "confirm": {"titogo"}} |
| + if status, _, out := postGated(t, cx.consoleFixture, cx.browser, cx.ts.URL+"/a/bab/trash/delete", v); status != http.StatusAccepted { |
| + t.Fatalf("purge answered %d: %s", status, out) |
| + } |
| + a, err := hostops.ReadCanonicalAdmission(context.Background(), cx.rel, "bab", "titogo") |
| + if err != nil || a.Value.Op == nil { |
| + t.Fatalf("no accepted purge: %v", err) |
| + } |
| + op, appID := a.Value.Op.ID, a.Value.AppID |
| + cx.publishPurgeCensus(t, op) |
| + injected := false |
| + for i := 0; i < 12 && cx.ownsPurge(t, "bab", "titogo", op); i++ { |
| + if j, tag, err := hostops.ReadPurgeJournal(context.Background(), cx.rel, "bab", "titogo", op, appID); err == nil && j.Phase == "retired" && !injected { |
| + if j.Hosts == nil { |
| + j.Hosts = map[string]hostops.PurgeHost{} |
| + } |
| + j.Hosts["gone.example.com"] = hostops.PurgeHost{Holder: &hostclaim.Holder{Incarnation: "inc-gone"}, Retired: true} |
| + if _, err := cx.srv.cfg.ClaimAuthority.Control.SavePurgeJournal(context.Background(), j, tag); err != nil { |
| + t.Fatalf("record the retired host in the journal: %v", err) |
| + } |
| + injected = true |
| + } |
| + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) |
| + if err := cx.srv.RunClaimRecoveryPass(ctx); err != nil { |
| + t.Logf("purge %s recovery pass %d: %v", op, i, err) |
| + } |
| + cancel() |
| + } |
| + if !injected { |
| + t.Fatal("the purge never stood at retired") |
| + } |
| + if cx.ownsPurge(t, "bab", "titogo", op) { |
| + t.Fatalf("purge %s did not finish", op) |
| + } |
| + if n := moveLogRows(t, cx.st, "gone.example.com", "mv_1"); n != 0 { |
| + t.Fatalf("%d rows of the purged incarnation remain", n) |
| + } |
| + if n := moveLogRows(t, cx.st, "gone.example.com", "mv_2"); n != 1 { |
| + t.Fatalf("another incarnation's rows: %d, want 1", n) |
| + } |
| +} |