| index b6c3153b0..498b6002a 100644 |
| --- a/internal/host/movesevidence_test.go |
| +++ b/internal/host/movesevidence_test.go |
| @@ -223,46 +223,164 @@ func TestVerifyRetirementLabelsMovesResidueWithTheProofsIncarnation(t *testing.T |
| } |
| } |
| |
| -// TestVerifyRetirementRefusesLegacyResidue (M7): residue under |
| -// instances/<host> outside the holder's live location has no moves-owned |
| -// archived-incarnation history. VerifyRetirement refuses it with a typed |
| -// dependency error (the legacy census is claim Plan 12's), which skips the |
| -// whole at-rest sweep. The moves proof itself is unaffected. |
| -func TestVerifyRetirementRefusesLegacyResidue(t *testing.T) { |
| +// retirement runs the claim's retirement answer, the one the regional worker |
| +// binds to every instances/<host> candidate, over the real census with this |
| +// fixture's provider as the claim's moves evidence. |
| +func (f *evFixture) retirement(t *testing.T, evidence hostbox.RegionalEvidence) (string, time.Time, error) { |
| + t.Helper() |
| + p := productionClaimRegional{Now: func() time.Time { return evNow }, Parks: &claimMoveParkSources{Evidence: evidence}} |
| + return p.retirement(t.Context(), f.observe(t), f.census(t)) |
| +} |
| + |
| +// TestRetirementLeavesQualifiedResidueToTheCensus (Q4): residue under |
| +// instances/<host> outside the holder's live location is the claim's census |
| +// to place, not the moves provider's to refuse. The census labels a |
| +// qualified path by its own identity (ClaimRetirementCensus) and the claim's |
| +// view refuses unmapped bare history per location, so the provider answers |
| +// with the move's close and the moves/<host> lane sweep goes ahead. |
| +func TestRetirementLeavesQualifiedResidueToTheCensus(t *testing.T) { |
| + older := strings.Repeat("ab", 32) |
| for name, key := range map[string]string{ |
| "a legacy level object": "instances/" + evHost + "/0000/db", |
| "a legacy generation object": "instances/" + evHost + "/g-old/0000/db", |
| - "an older incarnation": "instances/" + evHost + "/" + strings.Repeat("ab", 32) + "/g-old/0000/db", |
| + "an older incarnation": "instances/" + evHost + "/" + older + "/g-old/0000/db", |
| } { |
| t.Run(name, func(t *testing.T) { |
| f := newEvFixture(t) |
| f.lane(evMoveID, "move.json") |
| f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| - f.bucket.Put("data", key, []byte("legacy")) |
| - obs, census := f.observe(t), f.census(t) |
| - inc, at, err := f.provider().VerifyRetirement(t.Context(), obs, census) |
| - var dep *ClaimDependencyError |
| - if !errors.As(err, &dep) || inc != "" || !at.IsZero() { |
| - t.Fatalf("VerifyRetirement = %q, %v, %v; want a *ClaimDependencyError and no evidence", inc, at, err) |
| + f.bucket.Put("data", key, []byte("residue")) |
| + inc, at, err := f.retirement(t, f.provider()) |
| + if err != nil || inc != f.inc || !at.Equal(evAt) { |
| + t.Fatalf("retirement = %q, %v, %v; want %q, %v and no refusal", inc, at, err, f.inc, evAt) |
| } |
| - if _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census); err != nil { |
| + if _, err := f.provider().VerifyMovesSweep(t.Context(), f.observe(t), f.census(t)); err != nil { |
| t.Fatalf("the moves proof does not depend on instances residue: %v", err) |
| } |
| }) |
| } |
| + t.Run("the census labels a qualified path by its own identity", func(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| + f.bucket.Put("data", "instances/"+evHost+"/"+older+"/g-old/0000/db", []byte("older")) |
| + labels, err := ClaimRetirementCensus(f.observe(t), f.census(t)) |
| + if err != nil || labels["instances/"+evHost+"/"+older] != older { |
| + t.Fatalf("labels = %v, %v", labels, err) |
| + } |
| + if _, _, err := f.retirement(t, f.provider()); err != nil { |
| + t.Fatal(err) |
| + } |
| + }) |
| t.Run("an incarnation directory beside a bare holder", func(t *testing.T) { |
| f := newEvFixture(t) |
| f.bare(t) |
| f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| f.bucket.Put("data", "instances/"+evHost+"/"+f.inc+"/g-old/0000/db", []byte("scoped")) |
| - _, _, err := f.provider().VerifyRetirement(t.Context(), f.observe(t), f.census(t)) |
| - var dep *ClaimDependencyError |
| - if !errors.As(err, &dep) { |
| - t.Fatalf("err = %v, want a *ClaimDependencyError", err) |
| + inc, at, err := f.retirement(t, f.provider()) |
| + if err != nil || inc != f.inc || !at.Equal(evAt) { |
| + t.Fatalf("retirement = %q, %v, %v", inc, at, err) |
| } |
| }) |
| } |
| |
| +// TestRetirementStillRefusesAGenerationTheMoveDidNotRetire (Q4): the close |
| +// is the evidence time for every instances/<host> candidate the worker binds |
| +// it to, so at the live location it must be true. retiredGenerations stays: |
| +// a generation admitted from the move's own admission on, or written after |
| +// the close, is refused with *NotMovesEvidence. A generation the move did |
| +// retire passes beside it. |
| +func TestRetirementStillRefusesAGenerationTheMoveDidNotRetire(t *testing.T) { |
| + refused := func(t *testing.T, f *evFixture, gen string) { |
| + t.Helper() |
| + inc, at, err := f.retirement(t, f.provider()) |
| + var n *NotMovesEvidence |
| + if !errors.As(err, &n) || n.Generation != gen || inc != "" || !at.IsZero() { |
| + t.Fatalf("retirement = %q, %v, %v; want *NotMovesEvidence for %q and no evidence", inc, at, err, gen) |
| + } |
| + if !errors.Is(err, ErrNotMovesEvidence) { |
| + t.Fatalf("err does not unwrap to ErrNotMovesEvidence: %v", err) |
| + } |
| + } |
| + t.Run("a generation the move retired passes", func(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| + f.bucket.Put("data", f.genKey("old"), []byte("superseded by the move")) |
| + inc, at, err := f.retirement(t, f.provider()) |
| + if err != nil || inc != f.inc || !at.Equal(evAt) { |
| + t.Fatalf("retirement = %q, %v, %v", inc, at, err) |
| + } |
| + }) |
| + t.Run("a generation admitted after the move", func(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.setClaim(t, func(c *hostclaim.Claim) { |
| + c.Holder.Admissions = evAdmissions(evMoveID, "next") |
| + c.Holder.Generation = "next" |
| + }) |
| + f.bucket.Put("data", f.genKey("next"), []byte("live")) |
| + f.bucket.Put("data", f.genKey(evMoveID), []byte("superseded after the move")) |
| + refused(t, f, evMoveID) |
| + }) |
| + t.Run("an object written after the close", func(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.bucket.Put("data", f.genKey(evMoveID), []byte("live")) |
| + f.bucket.Put("data", f.genKey("old"), []byte("late")) |
| + f.rec.LastMove.At = time.Date(2026, 10, 5, 0, 0, 0, 0, time.UTC) |
| + refused(t, f, "old") |
| + }) |
| + t.Run("a claim that holds no admission of the move", func(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.setClaim(t, func(c *hostclaim.Claim) { c.Holder.Admissions = evAdmissions()[:1]; c.Holder.Generation = "old" }) |
| + f.bucket.Put("data", f.genKey("old"), []byte("live")) |
| + f.bucket.Put("data", f.genKey("other"), []byte("no admission to date it by")) |
| + refused(t, f, "other") |
| + }) |
| +} |
| + |
| +// TestJoiningClaimRefusesQuietly (Q4): a joining claim (a parking sweep with |
| +// no holder) and a free one name no record, so the moves provider has no |
| +// close to prove from. It refuses with a *MovesSweepRefusal that is not an |
| +// anomaly, never a dependency, and reads no record: the agent logs it once an |
| +// hour and pages nobody. The claim's own free-host proof answers a free claim |
| +// before it gets here (claimFreeMoveProof). |
| +func TestJoiningClaimRefusesQuietly(t *testing.T) { |
| + f := newEvFixture(t) |
| + f.setClaim(t, func(c *hostclaim.Claim) { |
| + c.Holder, c.Touched = nil, hostclaim.Touched{} |
| + c.Op = hostclaim.Op{Kind: hostclaim.KindParking, ID: "op", Intent: map[string]string{"park_sweep": "v1"}} |
| + c.Obligations = []hostclaim.Obligation{{ID: "chore", Op: "op", Kind: "park-replica", Region: "ie", Owner: "region:ie", Retirement: "ret", |
| + Park: &hostclaim.ParkScope{Location: f.location, Incarnation: f.inc, EvidenceAt: evAt, RequestLimit: hostbox.RequestLimit}}} |
| + }) |
| + f.lane(evMoveID, "move.json") |
| + var pages pagesSeen |
| + l := &refusalLog{now: evNow} |
| + r := newMovesRefusals("ie-box", pages.hook(), l.logf, l.clock) |
| + for name, evidence := range map[string]hostbox.RegionalEvidence{"the provider": f.provider(), "the quiet wrapper": r.evidence(f.provider())} { |
| + t.Run(name, func(t *testing.T) { |
| + inc, at, err := f.retirement(t, evidence) |
| + var dep *ClaimDependencyError |
| + if errors.As(err, &dep) { |
| + t.Fatalf("a dependency, not a refusal: %v", err) |
| + } |
| + wantRefusal(t, err, false) |
| + if inc != "" || !at.IsZero() { |
| + t.Fatalf("evidence %q, %v with a refusal", inc, at) |
| + } |
| + }) |
| + } |
| + if f.reads != 0 { |
| + t.Fatalf("read %d records with no holder to name one", f.reads) |
| + } |
| + _, _, err := f.retirement(t, f.provider()) |
| + r.report(err) |
| + r.report(err) |
| + if got := l.take(); len(got) != 1 { |
| + t.Fatalf("logged %q; want one line within the hour", got) |
| + } |
| + if len(pages) != 0 { |
| + t.Fatalf("a joining claim paged: %+v", pages) |
| + } |
| +} |
| + |
| // bare puts the holder at the activator's own lease location, |
| // instances/<host>: the only holder a move cuts (ruling T5-lease, |
| // movescut.go writerLease), so every moved host's holder. |
| @@ -547,12 +665,11 @@ func TestMovesEvidenceRefusesAnotherIdentity(t *testing.T) { |
| f.setClaim(t, func(c *hostclaim.Claim) { c.Holder, c.Op = nil, hostclaim.Op{} }) |
| obs, census := f.observe(t), f.census(t) |
| _, err := f.provider().VerifyMovesSweep(t.Context(), obs, census) |
| - var dep *ClaimDependencyError |
| - if !errors.As(err, &dep) { |
| - t.Fatalf("err = %v, want a *ClaimDependencyError", err) |
| - } |
| - if _, _, err := f.provider().VerifyRetirement(t.Context(), obs, census); !errors.As(err, &dep) { |
| - t.Fatalf("err = %v, want a *ClaimDependencyError", err) |
| + wantRefusal(t, err, false) |
| + if _, _, err := f.provider().VerifyRetirement(t.Context(), obs, census); err == nil { |
| + t.Fatal("VerifyRetirement answered with no holder") |
| + } else { |
| + wantRefusal(t, err, false) |
| } |
| if f.reads != 0 { |
| t.Fatalf("read %d records with no holder to name one", f.reads) |
| @@ -727,35 +844,45 @@ func TestSweepRefusedUnlessTheCloseIsBeforeTheListing(t *testing.T) { |
| } |
| |
| // TestSweepScopeMembership (witness wSweepScopeMembership, M7): residue of |
| -// an older incarnation is never the moves/<host> chore's. Without its own |
| -// evidence the provider refuses the whole sweep, so nothing is queued. |
| +// an older incarnation is never the moves/<host> chore's. The provider no |
| +// longer refuses the sweep for it (Q4): the engine queues it as its own |
| +// chore under its own qualified identity and parks it, and the moves/<host> |
| +// chore keeps the holder's. |
| func TestSweepScopeMembership(t *testing.T) { |
| f := newEvFixture(t) |
| + older := strings.Repeat("cd", 32) |
| f.lane(evMoveID, "move.json") |
| - f.bucket.Put("data", "instances/"+evHost+"/"+strings.Repeat("cd", 32)+"/g-old/0000/db", []byte("late")) |
| - err := f.worker(t, evNow).Run(t.Context()) |
| - var dep *ClaimDependencyError |
| - if !errors.As(err, &dep) { |
| - t.Fatalf("err = %v, want the legacy census dependency", err) |
| + residue := "instances/" + evHost + "/" + older + "/g-old/0000/db" |
| + f.bucket.Put("data", residue, []byte("late")) |
| + if err := f.worker(t, evNow).Run(t.Context()); !errors.Is(err, park.ErrPending) { |
| + t.Fatalf("the chores wait for their horizons: %v", err) |
| } |
| - if len(f.chores(t)) != 0 { |
| - t.Fatalf("chores %+v", f.chores(t)) |
| + chores := f.chores(t) |
| + moves, ok := chores["moves/"+evHost] |
| + if !ok || moves.Incarnation != f.inc { |
| + t.Fatalf("chores = %+v; want the moves/<host> chore under the holder's incarnation", chores) |
| + } |
| + if _, ok := chores["instances/"+evHost+"/"+older]; ok || len(chores) != 1 { |
| + t.Fatalf("chores = %+v; want only the moves/<host> chore pending", chores) |
| + } |
| + // The older incarnation's chore ran first, under its own identity, and |
| + // parked its residue in the same pass. |
| + if _, ok := f.bucket.Current("data", residue); ok { |
| + t.Fatal("the older incarnation's residue was not parked") |
| } |
| } |
| |
| -// TestFreeHostLaneProofIsClaimPlan12s (fix round 1, ruling 3): a free claim |
| -// names no record, so moves can prove no closed move and the claim stays |
| -// free. The free-host lane proof is transferred to claim Plan 12 (confirmed |
| -// by platform-1e); this test pins the refusal until then. |
| -func TestFreeHostLaneProofIsClaimPlan12s(t *testing.T) { |
| +// TestFreeHostLanesAreTheClaimsProof (fix round 1, ruling 3; Q4): a free |
| +// claim names no record, so moves can prove no closed move and the claim |
| +// stays free. The free-host lane proof is the claim's own |
| +// (claimFreeMoveProof, asked before the provider); a free claim that |
| +// reaches the provider gets the quiet sweep refusal, not a dependency. |
| +func TestFreeHostLanesAreTheClaimsProof(t *testing.T) { |
| f := newEvFixture(t) |
| f.setClaim(t, func(c *hostclaim.Claim) { c.Holder, c.Op, c.Touched = nil, hostclaim.Op{}, hostclaim.Touched{} }) |
| f.lane(evMoveID, "move.json") |
| err := f.worker(t, evNow).Run(t.Context()) |
| - var dep *ClaimDependencyError |
| - if !errors.As(err, &dep) { |
| - t.Fatalf("err = %v, want a *ClaimDependencyError", err) |
| - } |
| + wantRefusal(t, err, false) |
| if c := f.observe(t).Claim; !c.Free() || len(c.Obligations) != 0 { |
| t.Fatalf("claim = %+v", c) |
| } |