carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 385d3eb4a3ff70af64eef4cb94d82c688349937a parent b21f6f2
4 files changed, +98 −12
  • internal/host/claimretryrecovery_test.go +12 −1
  • internal/host/moveshome_fixture_test.go +46 −1
  • internal/host/movesparks_test.go +24 −2
  • internal/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)