moves: home target fixtures, fix round 1 (home part 2)
The production probe mode binds the owner dispatch's AdmissionEvidence into the writer before the runtime is built (Build captures the writer, so the controller's allow-all evidence stayed in force), with the writer's home alias. A rejected evidence admits nothing, and the writer's evidence refuses an admission the owners never selected. The retained residue is an earlier stay's park-generation scoped to the holder's location, the shape QueueResidueParks queues, not a whole-location park-replica.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+98
−12
internal/host/claimretryrecovery_test.go+12 −1internal/host/moveshome_fixture_test.go+46 −1internal/host/movesparks_test.go+24 −2internal/host/movesphase1_harness_test.go+16 −8
diff --git a/internal/host/claimretryrecovery_test.go b/internal/host/claimretryrecovery_test.go| index 24b6fdc12..fd21b9772 100644 |
| --- a/internal/host/claimretryrecovery_test.go |
| +++ b/internal/host/claimretryrecovery_test.go |
| @@ -134,6 +134,13 @@ type recoveryFixture struct { |
| // fixture sets its target box's (home part 2: home-box in its roster |
| // region ie). |
| box, enrollment, region, leaseRegion string |
| + // homeRegion is the writer's verified home alias (hostbox.Writer's |
| + // HomeRegion), "" for none. admissionEvidence, when set, is the |
| + // writer's AdmissionEvidence: the claim's owner dispatch in production |
| + // (claimassembly.go), bound before the runtime is built because Build |
| + // captures the writer. Unset, every admission's evidence passes. |
| + homeRegion string |
| + admissionEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error |
| } |
| // identity is the controller's box identity, the defaults filled in. |
| @@ -286,7 +293,11 @@ func (f *recoveryFixture) controller(boot, controller string, pid int, start str |
| } |
| a.bindClaimWakeStop() |
| source := hostbox.WorkSource{Guard: f.guard, Box: box, Region: region, Now: func() time.Time { return f.now }, Policy: func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error { return nil }, Local: func(context.Context, *hostclaim.Claim, hostbox.Selection) error { return nil }} |
| - writer := hostbox.Writer{Data: f.data, Bucket: f.bucket, Claims: f.claims, Box: box, Region: region, AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }} |
| + evidence := f.admissionEvidence |
| + if evidence == nil { |
| + evidence = func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil } |
| + } |
| + writer := hostbox.Writer{HomeRegion: f.homeRegion, Data: f.data, Bucket: f.bucket, Claims: f.claims, Box: box, Region: region, AdmissionEvidence: evidence} |
| runtime, err := w.Build(t.Context(), source, writer) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/moveshome_fixture_test.go b/internal/host/moveshome_fixture_test.go| index ede987594..be53b067c 100644 |
| --- a/internal/host/moveshome_fixture_test.go |
| +++ b/internal/host/moveshome_fixture_test.go |
| @@ -3,9 +3,11 @@ package host |
| import ( |
| "context" |
| "encoding/json" |
| + "errors" |
| "slices" |
| "testing" |
| + "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/park" |
| "github.com/carlosframework/platform/internal/provision" |
| @@ -104,7 +106,8 @@ func TestIntoHomeFixtureIsAHomeTarget(t *testing.T) { |
| chore = &o |
| } |
| } |
| - if chore == nil || chore.Region != "ie" || chore.Owner != "region:ie" || chore.Kind != "park-replica" { |
| + if chore == nil || chore.Region != "ie" || chore.Owner != "region:ie" || chore.Kind != "park-generation" || chore.Generation != retainedChoreGeneration || |
| + chore.Park == nil || chore.Park.Location != c.Holder.Location || chore.Park.Incarnation != h.inc { |
| t.Fatalf("%s: the earlier residue chore: %+v", spelling, c.Obligations) |
| } |
| if c.Op.Intent["to"] != park.HomeRegion { |
| @@ -134,6 +137,11 @@ func TestIntoHomeFixtureIsAHomeTarget(t *testing.T) { |
| if c.Holder.WriterBox != "home-box" || c.Op.Intent["to"] != park.HomeRegion { |
| t.Fatalf("the claim after the probe: writer %q to %q", c.Holder.WriterBox, c.Op.Intent["to"]) |
| } |
| + // The writer's evidence was the owners': asked once, for the one |
| + // admission. |
| + if p.evidence != 1 { |
| + t.Fatalf("the writer's admission evidence ran %d times, want 1", p.evidence) |
| + } |
| // Materialize ran the production owners' held steps, in order. |
| if want := []string{"render", "select:held-config", "select:held-release", "release:v7"}; len(p.order) < len(want) || !slices.Equal(p.order[:len(want)], want) { |
| t.Fatalf("the owners' held steps: %v", p.order) |
| @@ -141,6 +149,43 @@ func TestIntoHomeFixtureIsAHomeTarget(t *testing.T) { |
| h.committedAndOpen(t) |
| }) |
| + t.Run("rejected admission evidence admits nothing", func(t *testing.T) { |
| + h := newCloseHarness(t, intoHome) |
| + p := h.productionProbe(t) |
| + before := h.claim(t).Holder |
| + p.rejectEvidence = hostbox.ErrChanged |
| + h.land(t) |
| + if p.evidence == 0 { |
| + t.Fatal("the writer never asked for admission evidence") |
| + } |
| + after := h.claim(t).Holder |
| + if len(p.admissions) != 0 || len(after.Admissions) != len(before.Admissions) || after.Session != before.Session || after.WriterBox == "home-box" { |
| + t.Fatalf("an admission went through rejected evidence: %+v (before %+v)", after, before) |
| + } |
| + if m := h.read(t, p1Host).Move; m.State != provision.MoveLanding || m.LandingPhase != provision.LandProbing { |
| + t.Fatalf("the landing moved past probing: %+v", m) |
| + } |
| + // The owners' own evidence refuses an admission Select never made: |
| + // the dispatch the writer calls is the owners'. |
| + p.rejectEvidence = nil |
| + o, err := hostclaim.Read(t.Context(), h.s, p1Host) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + in, err := p.owners.Select(t.Context(), h.route, o, poProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := p.writerEvidence(t.Context(), o, in.Admission, in.Baseline); err != nil { |
| + t.Fatalf("the writer's evidence refused the owners' own admission: %v", err) |
| + } |
| + bad := in.Admission |
| + bad.Box = "ca-box" |
| + if err := p.writerEvidence(t.Context(), o, bad, in.Baseline); !errors.Is(err, hostbox.ErrChanged) { |
| + t.Fatalf("the writer's evidence accepted an admission on %s: %v", bad.Box, err) |
| + } |
| + }) |
| + |
| t.Run("the probe and recovery owners' fixtures", func(t *testing.T) { |
| f := newPOFixture(t, poIntoHome) |
| in, err := f.owners.Select(t.Context(), f.route, f.read(), poProbe) |
diff --git a/internal/host/movesparks_test.go b/internal/host/movesparks_test.go| index 42a8ab6f4..d35169b51 100644 |
| --- a/internal/host/movesparks_test.go |
| +++ b/internal/host/movesparks_test.go |
| @@ -17,6 +17,7 @@ import ( |
| "github.com/carlosframework/platform/internal/activator" |
| "github.com/carlosframework/platform/internal/appconfig" |
| + "github.com/carlosframework/platform/internal/genselect" |
| "github.com/carlosframework/platform/internal/hostbox" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostguard" |
| @@ -536,6 +537,13 @@ type prodProbe struct { |
| owners *movesProbeOwners |
| // order is the held works, render and release, in the order they ran. |
| order []string |
| + // evidence counts the writer's admission evidence calls, and |
| + // rejectEvidence, when set, refuses them before the owners are asked |
| + // (the rejected-evidence witness). |
| + evidence int |
| + rejectEvidence error |
| + // writerEvidence is the writer's AdmissionEvidence as bound. |
| + writerEvidence func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error |
| // admissions are the admissions the runtime made, one per attempt. |
| admissions []hostclaim.AdmissionRecord |
| last activator.ProbeResult |
| @@ -611,9 +619,23 @@ func (h *landingHarness) productionProbe(t *testing.T) *prodProbe { |
| t.Fatal("fixture: the production probe owners are not valid") |
| } |
| attempts := &movesProbeAttempts{Claims: h.s, Records: MovesRecordsFrom(h.s), DataDir: h.tdir, NewID: owners.NewID, Now: h.clock.Now, Admitted: MovesRecordAdmittedAttempts} |
| - c := f.controller("boot-1", "controller-1", 4101, "start-1") |
| bound := owners.MovesOwners(attempts) |
| - c.wake.Inputs = ClaimOwnerDispatch{Moves: &bound.ClaimKindOwners} |
| + dispatch := ClaimOwnerDispatch{Moves: &bound.ClaimKindOwners} |
| + // The writer's admission evidence is the owner dispatch's, as the claim |
| + // assembly binds it (claimassembly.go), bound before the runtime is |
| + // built: Build captures the writer, so a later swap would leave the |
| + // controller's allow-all evidence in force. |
| + f.homeRegion = actor.HomeRegion |
| + f.admissionEvidence = func(ctx context.Context, o hostclaim.Observed, in hostclaim.Admission, plan *genselect.Plan) error { |
| + p.evidence++ |
| + if p.rejectEvidence != nil { |
| + return p.rejectEvidence |
| + } |
| + return dispatch.AdmissionEvidence(ctx, o, in, plan) |
| + } |
| + p.writerEvidence = f.admissionEvidence |
| + c := f.controller("boot-1", "controller-1", 4101, "start-1") |
| + c.wake.Inputs = dispatch |
| c.wake.Continue = hostclaim.SessionContinues |
| c.wake.Operations = bound.Operations |
| c.wake.ProbeReady = func(context.Context, *exec.Cmd, registry.Route, ClaimLaunchInputs, string) error { return nil } |
diff --git a/internal/host/movesphase1_harness_test.go b/internal/host/movesphase1_harness_test.go| index 3f3583f64..652461398 100644 |
| --- a/internal/host/movesphase1_harness_test.go |
| +++ b/internal/host/movesphase1_harness_test.go |
| @@ -402,8 +402,12 @@ type p1Retained struct { |
| touched, quiet, chore string |
| } |
| -// retainedChoreOp is the earlier op whose residue chore retainChore seeds. |
| -const retainedChoreOp = "mv_earlierstayearlierstayearlie" |
| +// retainedChoreOp is the earlier op whose residue chore retainChore seeds, |
| +// and retainedChoreGeneration the earlier stay's generation it parks. |
| +const ( |
| + retainedChoreOp = "mv_earlierstayearlierstayearlie" |
| + retainedChoreGeneration = "old" |
| +) |
| // The fixture's enrollments (the roster's 32-hex tokens). |
| const ( |
| @@ -444,7 +448,9 @@ func retainTouched(region string) p1Option { |
| func retainQuiet(region string) p1Option { return func(h *phase1Harness) { h.retain.quiet = region } } |
| // retainChore seeds an earlier op's residue chore owed under region (the |
| -// roster name "ie" for home), after the start, as an earlier stay left it. |
| +// roster name "ie" for home), after the start, as an earlier stay left it: |
| +// a park-generation of the generation retainedChoreGeneration, scoped to |
| +// the holder's location, the shape hostclaim.QueueResidueParks queues. |
| func retainChore(region string) p1Option { return func(h *phase1Harness) { h.retain.chore = region } } |
| func newPhase1Harness(t *testing.T, opts ...p1Option) *phase1Harness { |
| @@ -642,12 +648,14 @@ func (h *phase1Harness) start(t *testing.T, cut string) provision.Record { |
| } |
| if h.retain.chore != "" { |
| // An earlier stay's residue chore in the home region, owed under |
| - // the spelling the earlier op wrote. Seeded after the start: the |
| - // start guard refuses a move into a region with a chore owed. |
| + // the spelling the earlier op wrote: one generation of the earlier |
| + // stay, parked under the location's scope, as QueueResidueParks |
| + // queues a landing's residue. Seeded after the start: the start |
| + // guard refuses a move into a region with a chore owed. |
| rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { |
| - c.Obligations = append(c.Obligations, hostclaim.Obligation{ID: "chore-earlier", Op: retainedChoreOp, Kind: "park-replica", Region: h.retain.chore, |
| - Owner: "region:" + h.retain.chore, Retirement: "retirement-earlier", |
| - Park: &hostclaim.ParkScope{Location: "instances/" + p1Host, Incarnation: h.inc, EvidenceAt: h.clock.Now().Add(-time.Hour).UTC(), RequestLimit: moves.ParkRequestLimit}}) |
| + c.Obligations = append(c.Obligations, hostclaim.Obligation{ID: "chore-earlier", Op: retainedChoreOp, Kind: "park-generation", Region: h.retain.chore, |
| + Generation: retainedChoreGeneration, Owner: "region:" + h.retain.chore, Retirement: "retirement-earlier", |
| + Park: &hostclaim.ParkScope{Location: c.Holder.Location, Incarnation: h.inc, EvidenceAt: h.clock.Now().Add(-time.Hour).UTC(), RequestLimit: moves.ParkRequestLimit}}) |
| }) |
| } |
| return h.read(t, p1Host) |