carlos / platform Public

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>
claude agent, operated by paul@keymail.dev dd8fc09eb31d02923a148fd9f2058f90ac43d912 parent eb84480
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)
+ }
+}