carlos / platform Public

moves: fixtures for a home target box (home part 2)

intoHome mirrors fromHome for the target: ca-box in ca to home-box
(placement "", data-home, eu-west-1, edges file with no placement,
held env with no CARLOS_REGION), roster home-box in ie with the fleet
home designation ie, op home_region ie through BeginAdmittedMove, and
ParkHomeRegion ie. Retained-state options seed touched, quiet_after
and an earlier residue chore in either spelling.

The close harness gains a production probe mode: the probe wakes
through movesProbeOwners over the claim runtime with home-box's roster
identity (region ie), the real C8 provider included. The fake probe
stays for the existing tests. The probe and recovery owners' fixtures
and the revert fixture accept a home target. Test-only; every default
is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev b21f6f23094e64e64f7e7eae99f84484607b4025 parent cf8e820
11 files changed, +804 −98
  • internal/host/claimretryrecovery_test.go +29 −5
  • internal/host/movescut_test.go +6 −6
  • internal/host/movesfinal_test.go +48 −21
  • internal/host/moveshome_fixture_test.go +195 −0
  • internal/host/movesland_probe_test.go +2 −2
  • internal/host/movesland_test.go +11 −11
  • internal/host/movesparks_test.go +267 −4
  • internal/host/movesphase1_harness_test.go +171 −19
  • internal/host/movesprobeowners_test.go +46 −8
  • internal/host/movesrecoveryowners_test.go +20 −13
  • internal/host/movesrevert_test.go +9 −9

CI passed — run details

CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok  	amadan.net/carlos/platform.git/launchctx	0.011s
?   	amadan.net/carlos/platform.git/launchctx/controlname	[no test files]
?   	amadan.net/carlos/platform.git/launchctx/wireid	[no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok  	github.com/carlosframework/platform	276.916s
?   	github.com/carlosframework/platform/cmd/changelog	[no test files]
ok  	github.com/carlosframework/platform/cmd/console	2.698s
ok  	github.com/carlosframework/platform/cmd/moveprobe	93.238s
ok  	github.com/carlosframework/platform/cmd/zddprobe	0.706s
ok  	github.com/carlosframework/platform/internal/activator	251.051s
ok  	github.com/carlosframework/platform/internal/agentsock	0.017s
ok  	github.com/carlosframework/platform/internal/alerting	0.004s
ok  	github.com/carlosframework/platform/internal/analytics	0.019s
ok  	github.com/carlosframework/platform/internal/appconfig	2.486s
ok  	github.com/carlosframework/platform/internal/apperr	0.071s
ok  	github.com/carlosframework/platform/internal/applog	0.128s
ok  	github.com/carlosframework/platform/internal/awsinv	0.004s
ok  	github.com/carlosframework/platform/internal/boxchoice	0.004s
ok  	github.com/carlosframework/platform/internal/boxid	0.749s
?   	github.com/carlosframework/platform/internal/boxid/boxidtest	[no test files]
?   	github.com/carlosframework/platform/internal/boxid/internal/signtest	[no test files]
ok  	github.com/carlosframework/platform/internal/boxvolumes	0.009s
ok  	github.com/carlosframework/platform/internal/caps	0.011s
ok  	github.com/carlosframework/platform/internal/certwarm	0.003s
ok  	github.com/carlosframework/platform/internal/cgroup	0.706s
ok  	github.com/carlosframework/platform/internal/changelog	0.302s
ok  	github.com/carlosframework/platform/internal/clamd	0.054s
?   	github.com/carlosframework/platform/internal/cliflag	[no test files]
ok  	github.com/carlosframework/platform/internal/console	385.018s
?   	github.com/carlosframework/platform/internal/controlname	[no test files]
?   	github.com/carlosframework/platform/internal/controlsock	[no test files]
ok  	github.com/carlosframework/platform/internal/dbdoor	6.379s
ok  	github.com/carlosframework/platform/internal/dbdoor/dbdoortest	0.009s
ok  	github.com/carlosframework/platform/internal/dbverify	0.004s
ok  	github.com/carlosframework/platform/internal/dnsrec	0.002s
ok  	github.com/carlosframework/platform/internal/docsite	0.005s
ok  	github.com/carlosframework/platform/internal/domains	2.364s
ok  	github.com/carlosframework/platform/internal/doorkey	0.006s
ok  	github.com/carlosframework/platform/internal/economics	0.003s
ok  	github.com/carlosframework/platform/internal/edge	26.567s
?   	github.com/carlosframework/platform/internal/edgeproof	[no test files]
ok  	github.com/carlosframework/platform/internal/edgesfile	0.008s
ok  	github.com/carlosframework/platform/internal/fleetapi	0.130s
ok  	github.com/carlosframework/platform/internal/gate	1.656s
ok  	github.com/carlosframework/platform/internal/genselect	0.014s
ok  	github.com/carlosframework/platform/internal/geo	0.015s
ok  	github.com/carlosframework/platform/internal/geoip	0.030s
ok  	github.com/carlosframework/platform/internal/host	135.150s
ok  	github.com/carlosframework/platform/internal/hostactors	0.378s
ok  	github.com/carlosframework/platform/internal/hostbox	2.204s
ok  	github.com/carlosframework/platform/internal/hostclaim	0.510s
ok  	github.com/carlosframework/platform/internal/hostevidence	0.014s
ok  	github.com/carlosframework/platform/internal/hostexec	4.759s
ok  	github.com/carlosframework/platform/internal/hostfence	0.030s
ok  	github.com/carlosframework/platform/internal/hostfiles	10.529s
ok  	github.com/carlosframework/platform/internal/hostfloor	0.013s
ok  	github.com/carlosframework/platform/internal/hostguard	0.969s
ok  	github.com/carlosframework/platform/internal/hostops	1.558s
?   	github.com/carlosframework/platform/internal/hostops/fencerelease	[no test files]
ok  	github.com/carlosframework/platform/internal/hostops/internal/fencerelease	0.013s
ok  	github.com/carlosframework/platform/internal/hostquarantine	0.013s
ok  	github.com/carlosframework/platform/internal/hostrelocate	0.434s
ok  	github.com/carlosframework/platform/internal/hostresolve	0.132s
ok  	github.com/carlosframework/platform/internal/hostwrite	0.014s
ok  	github.com/carlosframework/platform/internal/infraview	0.013s
ok  	github.com/carlosframework/platform/internal/instancectl	4.178s
ok  	github.com/carlosframework/platform/internal/instanceexec	2.719s
ok  	github.com/carlosframework/platform/internal/instancefs	0.018s
ok  	github.com/carlosframework/platform/internal/instancekey	0.015s
ok  	github.com/carlosframework/platform/internal/instanceupdates	0.023s
ok  	github.com/carlosframework/platform/internal/layout	1.498s
ok  	github.com/carlosframework/platform/internal/lease	9.791s
ok  	github.com/carlosframework/platform/internal/lease/leasetest	0.005s
ok  	github.com/carlosframework/platform/internal/ledger	0.013s
ok  	github.com/carlosframework/platform/internal/mail	0.161s
ok  	github.com/carlosframework/platform/internal/maintenance	0.077s
ok  	github.com/carlosframework/platform/internal/maintenanceexec	5.482s
ok  	github.com/carlosframework/platform/internal/maintenanceguard	0.064s
ok  	github.com/carlosframework/platform/internal/metrics	0.005s
ok  	github.com/carlosframework/platform/internal/moves	20.940s
ok  	github.com/carlosframework/platform/internal/moves/archive	0.753s
ok  	github.com/carlosframework/platform/internal/objstore	0.177s
ok  	github.com/carlosframework/platform/internal/ops	0.171s
ok  	github.com/carlosframework/platform/internal/park	0.194s
ok  	github.com/carlosframework/platform/internal/privatefile	0.004s
ok  	github.com/carlosframework/platform/internal/provision	15.457s
ok  	github.com/carlosframework/platform/internal/publicrelease	0.007s
?   	github.com/carlosframework/platform/internal/regionname	[no test files]
ok  	github.com/carlosframework/platform/internal/registry	4.876s
ok  	github.com/carlosframework/platform/internal/relay	7.236s
ok  	github.com/carlosframework/platform/internal/release	19.304s
ok  	github.com/carlosframework/platform/internal/rollout	3.650s
ok  	github.com/carlosframework/platform/internal/rolloutwords	0.003s
ok  	github.com/carlosframework/platform/internal/rootaws	0.006s
ok  	github.com/carlosframework/platform/internal/rootparticipation	0.006s
ok  	github.com/carlosframework/platform/internal/rootstate	0.025s
ok  	github.com/carlosframework/platform/internal/runs	0.577s
ok  	github.com/carlosframework/platform/internal/s3compat	0.004s
ok  	github.com/carlosframework/platform/internal/safefile	0.004s
ok  	github.com/carlosframework/platform/internal/schedule	2.481s
ok  	github.com/carlosframework/platform/internal/seeds	0.010s
ok  	github.com/carlosframework/platform/internal/sim/hostlife	9.121s
ok  	github.com/carlosframework/platform/internal/sim/litestreamfacts	3.738s
ok  	github.com/carlosframework/platform/internal/status	0.037s
ok  	github.com/carlosframework/platform/internal/steering	0.005s
ok  	github.com/carlosframework/platform/internal/stopevidence	0.215s
ok  	github.com/carlosframework/platform/internal/svcrole	0.002s
ok  	github.com/carlosframework/platform/internal/telemetry	0.004s
ok  	github.com/carlosframework/platform/internal/termfence	0.004s
ok  	github.com/carlosframework/platform/internal/tunnelclient	0.004s
ok  	github.com/carlosframework/platform/internal/unixdial	0.002s
ok  	github.com/carlosframework/platform/internal/updatewords	0.004s
ok  	github.com/carlosframework/platform/internal/usage	0.004s
ok  	github.com/carlosframework/platform/internal/version	0.001s
ok  	github.com/carlosframework/platform/internal/vet	1.551s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
linux/arm64 carlos binary: 46399650 bytes (ceiling 52000000)
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
  minio: cached, digest verified
  mc: cached, digest verified
  minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
  buckets and scoped user ready
=== RUN   TestDeliveredContractMinio
=== RUN   TestDeliveredContractMinio/put
=== RUN   TestDeliveredContractMinio/get_round-trips_the_body
=== RUN   TestDeliveredContractMinio/list_shows_the_key
=== RUN   TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN   TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN   TestDeliveredContractMinio/delete_removes_it
=== RUN   TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
    --- PASS: TestDeliveredContractMinio/put (0.00s)
    --- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
    --- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
    --- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
    --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
        --- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
    --- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
    --- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok  	github.com/carlosframework/platform/internal/objstore	0.022s
=== RUN   TestT4StorageProbeMinio
2026/10/15 16:12:43 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DEBFF5BD96F5D9, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok  	github.com/carlosframework/platform/internal/host	0.020s
minio contract: PASS
diff --git a/internal/host/claimretryrecovery_test.go b/internal/host/claimretryrecovery_test.go
index cbd0f51e3..24b6fdc12 100644
--- a/internal/host/claimretryrecovery_test.go
+++ b/internal/host/claimretryrecovery_test.go
@@ -129,6 +129,29 @@ type recoveryFixture struct {
restoreFailure error
recordPrelaunch bool
revision uint64
+ // box, enrollment, region and leaseRegion are the controller's box
+ // identity: box-1, enrollment-1, ie and eu-west-1 when unset. A moves
+ // fixture sets its target box's (home part 2: home-box in its roster
+ // region ie).
+ box, enrollment, region, leaseRegion string
+}
+
+// identity is the controller's box identity, the defaults filled in.
+func (f *recoveryFixture) identity() (box, enrollment, region, leaseRegion string) {
+ box, enrollment, region, leaseRegion = "box-1", "enrollment-1", "ie", "eu-west-1"
+ if f.box != "" {
+ box = f.box
+ }
+ if f.enrollment != "" {
+ enrollment = f.enrollment
+ }
+ if f.region != "" {
+ region = f.region
+ }
+ if f.leaseRegion != "" {
+ leaseRegion = f.leaseRegion
+ }
+ return box, enrollment, region, leaseRegion
}
type recoveryController struct {
@@ -217,10 +240,11 @@ func (f *recoveryFixture) controller(boot, controller string, pid int, start str
if revision == 0 {
revision = 7
}
- local := claimStopLocal{Actor: hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: revision, Region: "ie"}, BootID: boot, ControllerID: controller, PID: pid, ProcessStart: start}
+ box, enrollment, region, leaseRegion := f.identity()
+ local := claimStopLocal{Actor: hostbox.Actor{Box: box, Enrollment: enrollment, Revision: revision, Region: region}, BootID: boot, ControllerID: controller, PID: pid, ProcessStart: start}
f.peer = StopPeer{UID: 1001, PID: pid, ProcessStart: start, BootID: boot, ControllerID: controller}
producers := &ClaimProducerSet{}
- w := &ClaimWakeAssembly{LeaseRegion: "eu-west-1", Inputs: recoveryInputs{f}, Manager: new(cgroup.Manager)}
+ w := &ClaimWakeAssembly{LeaseRegion: leaseRegion, Inputs: recoveryInputs{f}, Manager: new(cgroup.Manager)}
w.AfterAdmission = func(context.Context, hostclaim.Observed, hostclaim.Observed) error { return nil }
// The unprivileged state-directory fixture runs under the real selected
// work; ColdRestore and its generation-marker writes remain unchanged.
@@ -250,7 +274,7 @@ func (f *recoveryFixture) controller(boot, controller string, pid int, start str
}
return &activator.RoleLaunch{Target: target, Journal: f.journal}, nil
}
- w.Retries = activator.ServingRetries{Claims: f.claims, Box: "box-1", Enrollment: "enrollment-1", BootID: boot, ControllerID: controller, Now: func() time.Time { return f.now }, NewID: f.newID, Inputs: func(context.Context, registry.Route) (string, error) { return "inputs", nil }}
+ w.Retries = activator.ServingRetries{Claims: f.claims, Box: box, Enrollment: enrollment, BootID: boot, ControllerID: controller, Now: func() time.Time { return f.now }, NewID: f.newID, Inputs: func(context.Context, registry.Route) (string, error) { return "inputs", nil }}
a := ClaimAssembly{Claims: f.claims, Launches: f.journal, Wake: w, Producers: producers, stopLocal: func(context.Context) (claimStopLocal, error) { return local, nil }}
a.collectStop = func(ctx context.Context, req StopRequest, drain StopDrainer) (StopResult, error) {
f.requests = append(f.requests, req)
@@ -261,8 +285,8 @@ func (f *recoveryFixture) controller(boot, controller string, pid int, start str
return validateTransportStop(req, result)
}
a.bindClaimWakeStop()
- source := hostbox.WorkSource{Guard: f.guard, Box: "box-1", Region: "ie", 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-1", Region: "ie", AdmissionEvidence: func(context.Context, hostclaim.Observed, hostclaim.Admission, *genselect.Plan) error { return nil }}
+ 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 }}
runtime, err := w.Build(t.Context(), source, writer)
if err != nil {
t.Fatal(err)
diff --git a/internal/host/movescut_test.go b/internal/host/movescut_test.go
index b28e0877e..5a3302b2d 100644
--- a/internal/host/movescut_test.go
+++ b/internal/host/movescut_test.go
@@ -120,7 +120,7 @@ func newCutHarness(t *testing.T, opts ...p1Option) *cutHarness {
if host != p1Host || prefix != cutLineage {
t.Errorf("checkpoint of %s at %s, want %s at the claim's lineage %s", host, prefix, p1Host, cutLineage)
}
- h.f.store("data-ie", prefix+lastTXKey, fakeMoveObject{lease.MirrorObject{ETag: "cur-c", Size: int64(len(last))}, string(last)})
+ h.f.store(h.srcBucket, prefix+lastTXKey, fakeMoveObject{lease.MirrorObject{ETag: "cur-c", Size: int64(len(last))}, string(last)})
return nil
}
h.cfg.LocalPosition = func(string) (uint64, error) { return h.localPos, nil }
@@ -154,7 +154,7 @@ func (h *cutHarness) target() moveCfg {
for k, v := range h.env {
env[k] = v
}
- env["CARLOS_REGION"] = "uk"
+ setHeldRegion(env, h.dst)
b := map[string]string{}
for k, v := range h.bundle {
b[k] = v
@@ -187,10 +187,10 @@ func (h *cutHarness) ready(t *testing.T) {
func rootHolder(t *testing.T, h *phase1Harness) {
t.Helper()
scoped := "instances/" + p1Host + "/" + h.inc + "/"
- for k, o := range h.f.buckets["data-ie"] {
+ for k, o := range h.f.buckets[h.srcBucket] {
if rel, ok := strings.CutPrefix(k, scoped); ok {
- delete(h.f.buckets["data-ie"], k)
- h.f.store("data-ie", cutRoot+"/"+rel, o)
+ delete(h.f.buckets[h.srcBucket], k)
+ h.f.store(h.srcBucket, cutRoot+"/"+rel, o)
}
}
rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) { c.Holder.Location = cutRoot })
@@ -701,7 +701,7 @@ func (h *cutHarness) writeOnStop(t *testing.T) {
func snapshotSealed(h *cutHarness) []string {
var out []string
- for k := range h.f.buckets["data-ie"] {
+ for k := range h.f.buckets[h.srcBucket] {
if strings.HasPrefix(k, moves.SnapshotsPrefix(p1Host, p1ID)) && strings.HasSuffix(k, ".sealed") {
out = append(out, k)
}
diff --git a/internal/host/movesfinal_test.go b/internal/host/movesfinal_test.go
index 269a9731e..9f0f80d3f 100644
--- a/internal/host/movesfinal_test.go
+++ b/internal/host/movesfinal_test.go
@@ -82,7 +82,7 @@ func newFinalHarnessFrom(t *testing.T, lh *landingHarness) *finalHarness {
t.Fatal(err)
}
}
- if _, err := h.s.Put(context.Background(), appconfig.ConfigAckKey("ie-box", p1Host), []byte(finalAckBody), ""); err != nil {
+ if _, err := h.s.Put(context.Background(), appconfig.ConfigAckKey(h.srcBox, p1Host), []byte(finalAckBody), ""); err != nil {
t.Fatal(err)
}
h.cfg.Route = func(host string) (*registry.Route, error) { return reg.Get(host) }
@@ -98,10 +98,11 @@ func newFinalHarnessFrom(t *testing.T, lh *landingHarness) *finalHarness {
return ok, err
}
h.cfg.Guard = hostguard.Guard{DataDir: dd, LockDir: t.TempDir()}
- h.cfg.Local = park.Local{DataDir: dd, Box: "ie-box"}
+ h.cfg.Local = park.Local{DataDir: dd, Box: h.srcBox}
h.cfg.ParkLeases = h.leases
- h.cfg.ParkView, h.cfg.ParkAuthorize = sourceTestView, sourceTestAuthorize
- h.cfg.ParkLocalAuthorize = sourceTestLocalAuthorize
+ h.cfg.ParkView = sourceTestViewFor(park.PlacementRegion(h.src), park.PlacementRegion(h.dst))
+ h.cfg.ParkAuthorize = sourceTestAuthorizeFor(h.srcBox, park.PlacementRegion(h.src))
+ h.cfg.ParkLocalAuthorize = sourceTestLocalAuthorizeFor(h.srcBox, park.PlacementRegion(h.src))
h.cfg.parkFault = h.lcfg.parkFault
h.cfg.finalFault = func(point string) error {
h.finalHits = append(h.finalHits, point)
@@ -162,7 +163,7 @@ func (h *finalHarness) commitClaim(t *testing.T) {
t.Fatal(err)
}
staging := hostclaim.ParkScope{Location: moves.ScopeLocation(p1Host, p1ID), Incarnation: h.inc, EvidenceAt: *rec.Move.FlippedAt, RequestLimit: cutRequestLimit}
- if _, err := hostclaim.Update(ctx, h.s, obs, hostclaim.Commit(p1ID, "ie", "ie-box", "uk", scope, staging, obligationID)); err != nil {
+ if _, err := hostclaim.Update(ctx, h.s, obs, hostclaim.Commit(p1ID, park.PlacementRegion(h.src), h.srcBox, park.PlacementRegion(h.dst), scope, staging, obligationID)); err != nil {
t.Fatal(err)
}
}
@@ -244,7 +245,7 @@ func (h *finalHarness) untouched(t *testing.T, why string) {
if m := h.read(t, p1Host).Move; m == nil || m.SourceParkedAt != nil {
t.Fatalf("%s: record %+v", why, m)
}
- if _, _, err := h.s.Get(context.Background(), appconfig.ConfigAckKey("ie-box", p1Host)); err != nil {
+ if _, _, err := h.s.Get(context.Background(), appconfig.ConfigAckKey(h.srcBox, p1Host)); err != nil {
t.Fatalf("%s: the config ack: %v", why, err)
}
for _, step := range []string{finalStepRemote, finalStepInstallation, finalStepLocal + "-plan", finalStepRowDeleted} {
@@ -866,31 +867,57 @@ func testSourcePaths() []string {
// under the location is selected; a chore is authorized only when it is
// this move's, for ie-box or ie, under the move's committed landing with
// ie-box its source.
-func sourceTestView(_ context.Context, obs hostclaim.Observed) (park.View, error) {
- if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil {
- return park.View{}, park.ErrUnsafe
+func sourceTestView(ctx context.Context, obs hostclaim.Observed) (park.View, error) {
+ return sourceTestViewFor("ie", "uk")(ctx, obs)
+}
+
+// sourceTestViewFor is sourceTestView for a source seen from region with
+// the target's region authoritative (the harness's sides: intoHome's is ca
+// with @home authoritative).
+func sourceTestViewFor(region, authoritative string) func(context.Context, hostclaim.Observed) (park.View, error) {
+ return func(_ context.Context, obs hostclaim.Observed) (park.View, error) {
+ if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil {
+ return park.View{}, park.ErrUnsafe
+ }
+ location := strings.TrimSuffix(obs.Claim.Holder.Location, "/")
+ return park.View{ClaimETag: obs.ETag, Region: region, AuthoritativeRegion: authoritative,
+ GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil
}
- location := strings.TrimSuffix(obs.Claim.Holder.Location, "/")
- return park.View{ClaimETag: obs.ETag, Region: "ie", AuthoritativeRegion: "uk",
- GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }}, nil
}
// sourceTestLocalAuthorize is the claim's local authorizer (M9) for the
// source's park-local: sourceTestAuthorize over the claim park.Local read.
func sourceTestLocalAuthorize(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error {
- return sourceTestAuthorize(ctx, hostclaim.Observed{Host: p1Host, Claim: c}, o)
+ return sourceTestLocalAuthorizeFor("ie-box", "ie")(ctx, c, o)
}
-func sourceTestAuthorize(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error {
- c := obs.Claim
- if obs.Absent || c == nil || c.Op.ID != p1ID || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseCommitted ||
- len(c.Op.Participants) != 2 || c.Op.Participants[0] != "ie-box" || o.Op != p1ID {
- return park.ErrUnsafe
+// sourceTestLocalAuthorizeFor is sourceTestLocalAuthorize for the source
+// box and its engine region.
+func sourceTestLocalAuthorizeFor(box, region string) func(context.Context, *hostclaim.Claim, hostclaim.Obligation) error {
+ authorize := sourceTestAuthorizeFor(box, region)
+ return func(ctx context.Context, c *hostclaim.Claim, o hostclaim.Obligation) error {
+ return authorize(ctx, hostclaim.Observed{Host: p1Host, Claim: c}, o)
}
- if (o.Kind == "park-local" && o.Box == "ie-box") || (o.Kind == "park-replica" && o.Region == "ie") {
- return nil
+}
+
+func sourceTestAuthorize(ctx context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error {
+ return sourceTestAuthorizeFor("ie-box", "ie")(ctx, obs, o)
+}
+
+// sourceTestAuthorizeFor is sourceTestAuthorize for the source box and its
+// engine region.
+func sourceTestAuthorizeFor(box, region string) func(context.Context, hostclaim.Observed, hostclaim.Obligation) error {
+ return func(_ context.Context, obs hostclaim.Observed, o hostclaim.Obligation) error {
+ c := obs.Claim
+ if obs.Absent || c == nil || c.Op.ID != p1ID || c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != hostclaim.PhaseCommitted ||
+ len(c.Op.Participants) != 2 || c.Op.Participants[0] != box || o.Op != p1ID {
+ return park.ErrUnsafe
+ }
+ if (o.Kind == "park-local" && o.Box == box) || (o.Kind == "park-replica" && o.Region == region) {
+ return nil
+ }
+ return park.ErrUnsafe
}
- return park.ErrUnsafe
}
// Fix round 1, ruling 1: a crash after source_parked_at is stamped and
diff --git a/internal/host/moveshome_fixture_test.go b/internal/host/moveshome_fixture_test.go
new file mode 100644
index 000000000..ede987594
--- /dev/null
+++ b/internal/host/moveshome_fixture_test.go
@@ -0,0 +1,195 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+ "slices"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/park"
+ "github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Moves into the home region, part 2, Task 2: the fixtures for a home
+// target box. intoHome mirrors fromHome for the target: the source is
+// ca-box in ca, the target home-box with placement "", whose roster name is
+// ie and whose fleet designates ie the home. The witnesses of Tasks 3 to 5
+// stand on these fixtures, so this test pins what each one is.
+
+var _ release.Store = p1FilesStore{}
+
+// TestIntoHomeFixtureIsAHomeTarget: the record's move names the home's
+// code, the target's edges file publishes no placement, the claim's op
+// names the placement-derived @home with home_region ie, the target's held
+// environment sets no CARLOS_REGION, and the close harness's production
+// probe mode admits on home-box in the claim engine's region ie.
+func TestIntoHomeFixtureIsAHomeTarget(t *testing.T) {
+ ctx := context.Background()
+
+ t.Run("the move, the edges file, the claim and the held env", func(t *testing.T) {
+ h := newPhase1Harness(t, intoHome)
+ rec := h.start(t, "manual")
+ if rec.Move == nil || rec.Move.To != "" || rec.Move.From != "ca" || rec.Region != "ca" {
+ t.Fatalf("the record: region %q move %+v", rec.Region, rec.Move)
+ }
+ body, _, err := h.s.Get(ctx, "control/edges/home-box.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ var edges map[string]any
+ if err := json.Unmarshal(body, &edges); err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := edges["placement"]; ok || edges["data_bucket"] != "data-home" || edges["box"] != "home-box" {
+ t.Fatalf("the home box's edges file: %s", body)
+ }
+ if _, _, err := h.s.Get(ctx, "control/edges/uk-box.json"); err == nil {
+ t.Fatal("a uk-box edges file is still published beside the home box")
+ }
+ c := h.claim(t)
+ if c.Op.Kind != hostclaim.KindMoving || c.Op.Intent["to"] != park.HomeRegion || c.Op.Intent["home_region"] != "ie" ||
+ c.Op.Intent["source"] != "ca-box" || c.Op.Intent["target"] != "home-box" || c.Op.Exec != "ca-box" {
+ t.Fatalf("the claim's op: %+v", c.Op)
+ }
+ if c.Op.Intent["source_enrollment"] != p1SrcEnrollment || c.Op.Intent["target_enrollment"] != p1DstEnrollment || c.Op.Intent["admission_id"] != p1ID {
+ t.Fatalf("the op was not begun as the claim-mode door begins it: %+v", c.Op.Intent)
+ }
+ a := h.roster[h.dstBox]
+ if a.Region != "ie" || a.FleetHomeRegion != "ie" || a.HomeRegion != "ie" || h.roster[h.srcBox].Region != "ca" || h.roster[h.srcBox].HomeRegion != "" {
+ t.Fatalf("the roster: %+v", h.roster)
+ }
+ tc := h.targetCfg()
+ if tc.Region != "" || tc.Box != "home-box" || tc.OwnBucket != "data-home" || tc.OwnBucketRegion != "eu-west-1" || tc.ParkHomeRegion != "ie" {
+ t.Fatalf("the target's configuration: region %q box %q bucket %q/%q park home %q", tc.Region, tc.Box, tc.OwnBucket, tc.OwnBucketRegion, tc.ParkHomeRegion)
+ }
+ env, _, _, err := tc.ComposeHeldEnv(ctx, h.s, rec, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := env["CARLOS_REGION"]; ok {
+ t.Fatal("the home target's held env sets CARLOS_REGION")
+ }
+ ch := newCutHarness(t, intoHome)
+ env, _, _, err = ch.target().ComposeHeldEnv(ctx, ch.s, rec, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, ok := env["CARLOS_REGION"]; ok {
+ t.Fatal("the cut harness's home target sets CARLOS_REGION")
+ }
+ // The default fixture is unchanged: uk sets its own region.
+ named := newPhase1Harness(t)
+ env, _, _, err = named.targetCfg().ComposeHeldEnv(ctx, named.s, rec, nil)
+ if err != nil || env["CARLOS_REGION"] != "uk" {
+ t.Fatalf("the named target's held env: %v %q", err, env["CARLOS_REGION"])
+ }
+ })
+
+ t.Run("retained state in either spelling", func(t *testing.T) {
+ for _, spelling := range []string{"ie", park.HomeRegion} {
+ h := newPhase1Harness(t, intoHome, retainTouched(spelling), retainQuiet(spelling), retainChore("ie"))
+ h.start(t, "manual")
+ c := h.claim(t)
+ if !containsString(c.Touched.Regions, spelling) || !containsString(c.Touched.Regions, "ca") {
+ t.Fatalf("%s: touched %+v", spelling, c.Touched)
+ }
+ if _, ok := c.QuietAfter[spelling]; !ok || len(c.QuietAfter) != 1 {
+ t.Fatalf("%s: quiet_after %v", spelling, c.QuietAfter)
+ }
+ var chore *hostclaim.Obligation
+ for _, o := range c.Obligations {
+ if o.Op == retainedChoreOp {
+ chore = &o
+ }
+ }
+ if chore == nil || chore.Region != "ie" || chore.Owner != "region:ie" || chore.Kind != "park-replica" {
+ t.Fatalf("%s: the earlier residue chore: %+v", spelling, c.Obligations)
+ }
+ if c.Op.Intent["to"] != park.HomeRegion {
+ t.Fatalf("%s: the op's to: %+v", spelling, c.Op.Intent)
+ }
+ }
+ })
+
+ t.Run("the production probe admits on home-box in ie", func(t *testing.T) {
+ h := newCloseHarness(t, intoHome)
+ if m := h.read(t, p1Host).Move; m.To != "" || m.State != provision.MoveLanding || m.LandingPhase != provision.LandRestored {
+ t.Fatalf("the close harness: %+v", m)
+ }
+ p := h.productionProbe(t)
+ h.land(t)
+ if len(p.admissions) != 1 {
+ t.Fatalf("admissions through the claim runtime: %+v", p.admissions)
+ }
+ a := p.admissions[0]
+ if a.Box != "home-box" || a.Region != "ie" || a.PredecessorRegion != "ca" || a.Mode != "cold" || a.Generation != p1ID || a.Coverage != "sealed" {
+ t.Fatalf("the probe's admission: %+v", a)
+ }
+ if p.owners.Box != "home-box" || p.owners.Region != "ie" || p.owners.HomeRegion != "ie" || p.owners.FleetHomeRegion != "ie" {
+ t.Fatalf("the owners' identity: box %q region %q home %q fleet %q", p.owners.Box, p.owners.Region, p.owners.HomeRegion, p.owners.FleetHomeRegion)
+ }
+ c := h.claim(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"])
+ }
+ // 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)
+ }
+ h.committedAndOpen(t)
+ })
+
+ 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)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if a := in.Admission; a.Box != "home-box" || a.Region != "ie" || a.PredecessorRegion != "ca" || a.Mode != "cold" {
+ t.Fatalf("the probe owners' admission: %+v", a)
+ }
+ if err := f.owners.AdmissionEvidence(t.Context(), f.read(), in.Admission, in.Baseline); err != nil {
+ t.Fatal(err)
+ }
+ if o := f.read(); o.Claim.Op.Intent["to"] != park.HomeRegion || o.Claim.Op.Intent["home_region"] != "ie" || f.rec.Move.To != "" {
+ t.Fatalf("the probe owners' fixture: %+v %+v", o.Claim.Op.Intent, f.rec.Move)
+ }
+ for _, kind := range []string{rcClean, rcDirty, rcRevert} {
+ f := newRCFixture(t, kind, poIntoHome)
+ o := f.read()
+ in := f.selectAdmission(o)
+ if a := in.Admission; a.Box != "ca-box" || a.Region != "ca" {
+ t.Fatalf("%s: the recovery owners' admission: %+v", kind, a)
+ }
+ if err := f.owners.AdmissionEvidence(t.Context(), o, in.Admission, in.Baseline); err != nil {
+ t.Fatalf("%s: %v", kind, err)
+ }
+ if o.Claim.Op.Intent["to"] != park.HomeRegion || o.Claim.Op.Intent["target"] != "home-box" {
+ t.Fatalf("%s: the recovery claim: %+v", kind, o.Claim.Op)
+ }
+ }
+ })
+
+ t.Run("the revert fixture", func(t *testing.T) {
+ for _, from := range []string{"stopping", "stopped"} {
+ h := newRevertHarness(t, intoHome)
+ if from == "stopping" {
+ h.fromStopping(t)
+ h.ev.stopped = true
+ } else {
+ h.fromStopped(t)
+ }
+ h.arm(t)
+ h.drive(t)
+ h.closed(t)
+ if rec := h.read(t, p1Host); rec.Region != "ca" || rec.LastMove == nil || rec.LastMove.Outcome != "rolled_back" {
+ t.Fatalf("%s: the record: %+v", from, rec)
+ }
+ if keys := h.laneKeys(); len(keys) != 0 {
+ t.Fatalf("%s: the home bucket's lane still holds %v", from, keys)
+ }
+ }
+ })
+}
diff --git a/internal/host/movesland_probe_test.go b/internal/host/movesland_probe_test.go
index 467141f59..c8f505932 100644
--- a/internal/host/movesland_probe_test.go
+++ b/internal/host/movesland_probe_test.go
@@ -61,7 +61,7 @@ func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activat
}
h := obs.Claim.Holder
gen := p1ID
- if h.WriterBox == "uk-box" {
+ if h.WriterBox == p.h.dstBox {
gen = "fresh" + strconv.Itoa(n)
}
// The predecessor's region is the probe owners' own rule: the last
@@ -71,7 +71,7 @@ func (p *probeLog) wake(ctx context.Context, cfg activator.ProbeConfig) (activat
return activator.ProbeResult{}, err
}
admitted, err := hostclaim.Update(ctx, p.h.s, obs, hostclaim.AdmitWriter(hostclaim.Admission{ObservedOpID: p1ID, ObservedPhase: provision.LandProbing,
- ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "uk-box", Mode: "cold", NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: "uk", PredecessorRegion: from}))
+ ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: p.h.dstBox, Mode: "cold", NewGeneration: gen, KeepStamp: h.Stamp > h.Session, Region: p.h.dstEngine, PredecessorRegion: from}))
if err != nil {
return activator.ProbeResult{}, err
}
diff --git a/internal/host/movesland_test.go b/internal/host/movesland_test.go
index f821cdd03..4e977c650 100644
--- a/internal/host/movesland_test.go
+++ b/internal/host/movesland_test.go
@@ -303,9 +303,9 @@ func newLandingHarness(t *testing.T, opts ...p1Option) *landingHarness {
client, n, rows := realReplica(t, 2)
h.n, h.rows = n, rows
loc := "instances/" + p1Host + "/" + h.inc + "/"
- for k := range h.f.buckets["data-ie"] {
+ for k := range h.f.buckets[h.srcBucket] {
if strings.HasPrefix(k, loc+"g-cur/") && strings.HasSuffix(k, ".ltx") {
- delete(h.f.buckets["data-ie"], k)
+ delete(h.f.buckets[h.srcBucket], k)
}
}
for level := 0; level <= litestream.SnapshotLevel; level++ {
@@ -320,7 +320,7 @@ func newLandingHarness(t *testing.T, opts ...p1Option) *landingHarness {
t.Fatal(err)
}
key := loc + "g-cur/" + fmt.Sprintf("%04x/", level) + ltx.FormatFilename(info.MinTXID, info.MaxTXID)
- h.f.store("data-ie", key, fakeMoveObject{lease.MirrorObject{ETag: "r-" + filepath.Base(key), Size: int64(len(body))}, string(body)})
+ h.f.store(h.srcBucket, key, fakeMoveObject{lease.MirrorObject{ETag: "r-" + filepath.Base(key), Size: int64(len(body))}, string(body)})
}
}
h.localPos = n
@@ -345,7 +345,7 @@ func newLandingHarness(t *testing.T, opts ...p1Option) *landingHarness {
return appconfig.HeldRender{Path: filepath.Join(h.tdir, p1Host+".json"), SHA256: "x", BundleETags: in.BundleETags, OverlayETag: in.OverlayETag}, nil
}
// The restore-verification record names the host: local removes it.
- if err := dbverify.Publish(context.Background(), h.s, &dbverify.Record{V: 1, Box: "uk-box", CheckedAt: h.clock.Now(),
+ if err := dbverify.Publish(context.Background(), h.s, &dbverify.Record{V: 1, Box: h.dstBox, CheckedAt: h.clock.Now(),
Hosts: map[string]dbverify.Verdict{p1Host: {OK: true, VerifiedAt: h.clock.Now()}, "other.example": {OK: true, VerifiedAt: h.clock.Now()}}}); err != nil {
t.Fatal(err)
}
@@ -371,9 +371,9 @@ func (h *landingHarness) landingCfg(t *testing.T) moveCfg {
return filepath.Join(h.tdir, host), filepath.Join(h.tdir, host+".env")
}
c.Guard = hostguard.Guard{DataDir: h.tdir, LockDir: t.TempDir()}
- c.Local = park.Local{DataDir: h.tdir, Box: "uk-box"}
+ c.Local = park.Local{DataDir: h.tdir, Box: h.dstBox}
inherited := c.ParkAuthorize
- armParkFakes(&c, h.s, h.leases, c.Region, roleTarget)
+ armParkFakes(&c, h.s, h.leases, park.PlacementRegion(c.Region), roleTarget)
if h.parkView != nil {
c.ParkView = func(ctx context.Context, obs hostclaim.Observed) (park.View, error) { return h.parkView(ctx, obs) }
}
@@ -484,7 +484,7 @@ func (h *landingHarness) flip(t *testing.T) {
h.ready(t)
h.cut(t)
h.pass(t)
- if rec := h.read(t, p1Host); rec.Region != "uk" || rec.Move.State != provision.MoveLanding || rec.Move.LandingPhase != provision.LandChecking {
+ if rec := h.read(t, p1Host); rec.Region != h.dst || rec.Move.State != provision.MoveLanding || rec.Move.LandingPhase != provision.LandChecking {
t.Fatalf("the fixture did not flip: %s %+v", rec.Region, rec.Move)
}
}
@@ -563,7 +563,7 @@ func (h *landingHarness) installed(t *testing.T) moves.InstallManifest {
t.Fatalf("record: %+v", m)
}
c := h.claim(t)
- if c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != provision.LandRestored || c.Op.Exec != "uk-box" || !containsString(c.Touched.Boxes, "uk-box") {
+ if c.Op.Kind != hostclaim.KindLanding || c.Op.Phase != provision.LandRestored || c.Op.Exec != h.dstBox || !containsString(c.Touched.Boxes, h.dstBox) {
t.Fatalf("claim: %+v touched %+v", c.Op, c.Touched)
}
j := h.landJournal(t)
@@ -811,7 +811,7 @@ func (h *landingHarness) residue(t *testing.T) []string {
cutRoot + "/generation.json",
}
for _, k := range keys {
- if _, err := h.vs.PutIfAbsent(ctx, "data-uk", k, []byte("residue "+k)); err != nil {
+ if _, err := h.vs.PutIfAbsent(ctx, h.dstBucket, k, []byte("residue "+k)); err != nil {
t.Fatal(err)
}
}
@@ -1031,7 +1031,7 @@ func (h *landingHarness) earlierStay(t *testing.T) {
t.Fatal(err)
}
rewriteP1Claim(t, h.s, func(c *hostclaim.Claim) {
- c.Inventory = map[string]hostclaim.InventoryEntry{"uk-box": {Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}}}
+ c.Inventory = map[string]hostclaim.InventoryEntry{h.dstBox: {Layout: "flat", Paths: map[string]uint64{p1Host + ".db": 1, p1Host + ".db.gen": 1, p1Host: 1, p1Host + ".env": 1}}}
})
if err := os.MkdirAll(filepath.Join(h.tdir, p1Host), 0o755); err != nil {
t.Fatal(err)
@@ -1288,7 +1288,7 @@ func landedPreparedManifest(t *testing.T, h *landingHarness) (preparedManifest,
if err := json.Unmarshal([]byte(raw), &pm); err != nil {
t.Fatalf("files_manifest %q: %v", raw, err)
}
- obj, ok := h.f.buckets["data-uk"][pm.Key]
+ obj, ok := h.f.buckets[h.dstBucket][pm.Key]
if !ok {
t.Fatalf("no manifest at %s in the target's bucket", pm.Key)
}
diff --git a/internal/host/movesparks_test.go b/internal/host/movesparks_test.go
index 7eb9ff6de..42a8ab6f4 100644
--- a/internal/host/movesparks_test.go
+++ b/internal/host/movesparks_test.go
@@ -3,17 +3,30 @@ package host
import (
"bytes"
"context"
+ "crypto/ed25519"
"encoding/json"
"errors"
+ "fmt"
+ "os"
+ "os/exec"
"path/filepath"
+ "sort"
"strings"
"testing"
"time"
+ "github.com/carlosframework/platform/internal/activator"
+ "github.com/carlosframework/platform/internal/appconfig"
+ "github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
+ "github.com/carlosframework/platform/internal/hostguard"
+ "github.com/carlosframework/platform/internal/hostwrite"
+ "github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
)
// The target's staging park after the commit and the close (moves spec §7.9
@@ -57,8 +70,9 @@ func (h *finalHarness) chore(t *testing.T, region string, lane bool) *hostclaim.
// engine over data-ie, run once at the harness's clock.
func (h *finalHarness) regionalAck(t *testing.T, o *hostclaim.Obligation) error {
t.Helper()
- e := park.Engine{Store: h.vs, Claims: h.s, Bucket: "data-ie", Region: "ie", Leases: h.leases, Now: h.clock.Now,
- NewID: obligationID, View: sourceTestView, Authorize: sourceTestAuthorize}
+ src, dst := park.PlacementRegion(h.src), park.PlacementRegion(h.dst)
+ e := park.Engine{Store: h.vs, Claims: h.s, Bucket: h.srcBucket, Region: src, Leases: h.leases, Now: h.clock.Now,
+ NewID: obligationID, View: sourceTestViewFor(src, dst), Authorize: sourceTestAuthorizeFor(h.srcBox, src)}
return e.Run(context.Background(), park.Work{Host: p1Host, ID: o.ID, Op: p1ID})
}
@@ -109,11 +123,11 @@ func (h *finalHarness) notClosed(t *testing.T, why string) {
func (h *finalHarness) done(t *testing.T) {
t.Helper()
rec := h.read(t, p1Host)
- if rec.Move != nil || rec.Region != "uk" || rec.LastMove == nil || rec.LastMove.ID != p1ID || rec.LastMove.Outcome != "done" {
+ if rec.Move != nil || rec.Region != h.dst || rec.LastMove == nil || rec.LastMove.ID != p1ID || rec.LastMove.Outcome != "done" {
t.Fatalf("the record after the close: region %s move %+v last %+v", rec.Region, rec.Move, rec.LastMove)
}
c := h.claim(t)
- if c.Op.Kind != hostclaim.KindServing || c.Op.Exec != "uk-box" || hostclaim.RequireReleasedMove(p1ID)(c) != nil || len(c.Obligations) != 0 {
+ if c.Op.Kind != hostclaim.KindServing || c.Op.Exec != h.dstBox || hostclaim.RequireReleasedMove(p1ID)(c) != nil || len(c.Obligations) != 0 {
t.Fatalf("the claim after the close: %+v obligations %+v", c.Op, c.Obligations)
}
}
@@ -411,3 +425,252 @@ func TestSourceFinalizationRenameIsGuarded(t *testing.T) {
})
}
}
+
+// The close harness's production probe mode (home part 2, Task 2). The
+// fake (probeLog.wake) admits with a literal box and region and so never
+// meets the production owners, their admission evidence or the C8 files
+// publication, which is where a home box's roster name meets the move's
+// @home. This mode wakes the target's probe through the production
+// movesProbeOwners over the claim runtime (claim Plan 9 Group D's
+// ClaimWakeAssembly, as TestMovesProbeOwnersThroughTheClaimRuntime drives
+// it), with the target box's identity from the roster: Box the target box,
+// Region its claim engine region (its roster name, ie for a home box),
+// HomeRegion and FleetHomeRegion the fleet's designation. Admit, Prepare,
+// the probe's open, the attempt's reservation and Materialize are the
+// claim's and the owners' own. What stands in is what main cannot run here:
+// the launch (every claim start is refused on main until the isolated root
+// launcher lands), the held works and render (the claim's move read access
+// over this box's registry), and the activator's live probe entry. A
+// Materialize that passes is the attempt passing.
+
+// p1Work is a held action's selected work in the production probe mode:
+// it runs fn, and records the identities it ran under.
+type p1Work struct {
+ order *[]string
+ name string
+}
+
+func (w p1Work) With(_ context.Context, id hostwrite.Identity, fn func() error) error {
+ *w.order = append(*w.order, w.name+":"+id.Config+"|"+id.Release)
+ return fn()
+}
+
+// p1Markers is the claim's marker store over the harness's fake versioned
+// buckets: a conditional put on the current version's etag.
+type p1Markers struct{ vs *fakeVersioned }
+
+func (m p1Markers) PutMarker(ctx context.Context, bucket, key string, body []byte, expected string, check func(context.Context) error) (string, error) {
+ if err := check(ctx); err != nil {
+ return "", err
+ }
+ current := ""
+ if o, ok := m.vs.f.buckets[bucket][key]; ok {
+ current = o.ETag
+ }
+ if current != expected {
+ return "", hostbox.ErrChanged
+ }
+ ver := m.vs.f.nextVersion()
+ m.vs.f.store(bucket, key, fakeMoveObject{lease.MirrorObject{ETag: "etag-" + ver, Size: int64(len(body))}, string(body)})
+ m.vs.f.setVersion(bucket, key, ver)
+ return ver, nil
+}
+
+// p1FilesStore is the target's data bucket as a release.Store for the C8
+// provider (movesFiles.Files): current reads with the version as the etag,
+// create-only and compare-and-swap writes.
+type p1FilesStore struct {
+ vs *fakeVersioned
+ bucket string
+}
+
+func (s p1FilesStore) Get(_ context.Context, key string) ([]byte, string, error) {
+ o, ok := s.vs.f.buckets[s.bucket][key]
+ if !ok {
+ return nil, "", os.ErrNotExist
+ }
+ return []byte(o.body), s.vs.versionID(s.bucket, key), nil
+}
+
+func (s p1FilesStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
+ if ifMatch == "" {
+ ver, err := s.vs.PutIfAbsent(ctx, s.bucket, key, body)
+ if errors.Is(err, lease.ErrExists) {
+ return "", release.ErrCASConflict
+ }
+ return ver, err
+ }
+ if _, ok := s.vs.f.buckets[s.bucket][key]; !ok || s.vs.versionID(s.bucket, key) != ifMatch {
+ return "", release.ErrCASConflict
+ }
+ ver := s.vs.f.nextVersion()
+ s.vs.f.store(s.bucket, key, fakeMoveObject{lease.MirrorObject{ETag: "etag-" + ver, Size: int64(len(body))}, string(body)})
+ s.vs.f.setVersion(s.bucket, key, ver)
+ return ver, nil
+}
+
+func (s p1FilesStore) List(_ context.Context, prefix string) ([]string, error) {
+ var out []string
+ for k := range s.vs.f.buckets[s.bucket] {
+ if strings.HasPrefix(k, prefix) {
+ out = append(out, k)
+ }
+ }
+ sort.Strings(out)
+ return out, nil
+}
+
+func (p1FilesStore) ListDirs(context.Context, string) ([]string, error) {
+ return nil, errors.New("test: not implemented")
+}
+
+func (p1FilesStore) Delete(context.Context, string) error {
+ return errors.New("test: not implemented")
+}
+
+// prodProbe is the production probe mode's state.
+type prodProbe struct {
+ t *testing.T
+ h *landingHarness
+ ctl *recoveryController
+ owners *movesProbeOwners
+ // order is the held works, render and release, in the order they ran.
+ order []string
+ // admissions are the admissions the runtime made, one per attempt.
+ admissions []hostclaim.AdmissionRecord
+ last activator.ProbeResult
+ live bool
+ seq int
+}
+
+// productionProbe switches the harness's probe to the production mode: the
+// target's probe wakes through movesProbeOwners over the claim runtime, with
+// the target box's roster identity.
+func (h *landingHarness) productionProbe(t *testing.T) *prodProbe {
+ t.Helper()
+ // Claim Plan 15: admissions through the real CAS record their origin
+ // under both switches; the real C8's proof reads that record.
+ t.Setenv("CARLOS_HOST_CLAIM", "1")
+ t.Setenv("CARLOS_HOST_CLAIM_ORIGINS", "1")
+ actor := h.roster[h.dstBox]
+ journal, err := hostbox.OpenLaunchJournal(filepath.Join(t.TempDir(), "launches.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { journal.Close() })
+ p := &prodProbe{t: t, h: h}
+ f := &recoveryFixture{t: t, claims: h.s, evidence: release.NewDirStore(t.TempDir()), signer: ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize)),
+ guard: hostguard.Guard{DataDir: h.tdir, LockDir: t.TempDir(), Claims: hostguard.StoreReader{Store: h.s}},
+ journal: journal, kernel: &recoveryKernel{journal: journal}, now: h.clock.Now(), recordPrelaunch: true,
+ data: h.vs, bucket: h.dstBucket, box: actor.Box, enrollment: actor.Enrollment, region: actor.Region, leaseRegion: h.dstAWS}
+ key, err := moves.ReportKeyFromSecrets(h.cfg.SecretsKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+ filesDir := h.lcfg.FilesDir
+ owners := &movesProbeOwners{HomeRegion: actor.HomeRegion, FleetHomeRegion: actor.FleetHomeRegion, Claims: h.s, Records: MovesRecordsFrom(h.s),
+ ReadyPath: movesReadyPath(h.s), Data: h.vs, Bucket: h.dstBucket, Region: actor.Region, Box: actor.Box, DataDir: h.tdir, FilesDir: filesDir,
+ Markers: p1Markers{h.vs},
+ Route: func(host string) (*registry.Route, error) { rt := h.route; return &rt, nil },
+ // The held row's launch: the environment the readiness pin was
+ // reported over (the source's launch report), the live release.
+ Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) {
+ env := make([]string, 0, len(h.env))
+ for k, v := range h.env {
+ env = append(env, k+"="+v)
+ }
+ sort.Strings(env)
+ return ClaimServingLaunch{Executable: "/opt/carlos/live/" + p1Host, ExecutableSHA256: strings.Repeat("a", 64), Env: env, Version: "v7"}, nil
+ },
+ EnvKey: key,
+ Admitted: MovesRecordAdmittedAttempts,
+ Works: func(_ context.Context, _, action string, _ hostguard.Captured) (hostwrite.Work, error) {
+ p.order = append(p.order, "select:"+action)
+ return p1Work{order: &p.order, name: action}, nil
+ },
+ Render: func(ctx context.Context, _ registry.Route, _ string, work func(appconfig.HeldRender) (hostwrite.Work, error)) (appconfig.HeldRender, error) {
+ p.order = append(p.order, "render")
+ in := h.read(t, p1Host).Move.Inputs
+ hr := appconfig.HeldRender{BundleETags: in.BundleETags, OverlayETag: in.OverlayETag}
+ if _, err := work(hr); err != nil {
+ return appconfig.HeldRender{}, err
+ }
+ return hr, nil
+ },
+ Release: func(ctx context.Context, _ registry.Route, version string, work hostwrite.Work) error {
+ p.order = append(p.order, "release:"+version)
+ return work.With(ctx, hostwrite.Identity{Release: version}, func() error { return nil })
+ },
+ NewID: func() string { p.seq++; return fmt.Sprintf("fresh%d", p.seq) },
+ Now: h.clock.Now}
+ // The C8 provider is the real one over the target's bucket, as the
+ // claim assembly binds it, with Plan 15's proof over the claim store.
+ owners.Files = &movesFiles{Files: p1FilesStore{vs: h.vs, bucket: h.dstBucket}, Data: h.vs, Bucket: h.dstBucket, HomeRegion: actor.FleetHomeRegion, Region: actor.Region, Box: actor.Box,
+ DataDir: h.tdir, EnvFile: envFileOf(filesDir), Proof: ClaimOriginProof{Claims: h.s, HomeRegion: actor.FleetHomeRegion, Region: actor.Region}}
+ if !owners.valid() {
+ 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}
+ 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 }
+ p.ctl, p.owners = c, owners
+ h.probe, h.probeLive = p.wake, p.liveProbe
+ return p
+}
+
+// wake is one probe attempt through the claim runtime: the activator's
+// ProbeWake with its start policy, as far as the claim runs on main.
+func (p *prodProbe) wake(ctx context.Context, cfg activator.ProbeConfig) (activator.ProbeResult, error) {
+ p.live = false
+ h := p.h
+ o, err := hostclaim.Read(ctx, h.s, p1Host)
+ if err != nil {
+ return activator.ProbeResult{}, err
+ }
+ policy := activator.StartPolicy{MaxStarts: 5, Window: time.Duration(moves.ProbeSeconds * float64(time.Second)), Probe: true,
+ Deadline: cfg.Deadline, Prepared: cfg.Prepared, Inputs: cfg.Inputs}
+ live, cancel := context.WithCancel(context.Background())
+ p.t.Cleanup(cancel)
+ done := make(chan struct{})
+ go func() { <-live.Done(); close(done) }()
+ p.ctl.wake.mu.Lock()
+ p.ctl.wake.pending[p1Host] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done}
+ p.ctl.wake.mu.Unlock()
+ rt := h.route
+ admitted, err := p.ctl.runtime.Admit(ctx, rt, o, policy)
+ if err != nil {
+ return activator.ProbeResult{}, err
+ }
+ ah := admitted.Claim.Holder
+ p.admissions = append(p.admissions, ah.Admissions[len(ah.Admissions)-1])
+ s, err := p.ctl.runtime.Prepare(ctx, rt, admitted, policy)
+ if err != nil {
+ return activator.ProbeResult{}, err
+ }
+ if s.OpenProbe != nil {
+ if _, err := s.OpenProbe(ctx, h.clock.Now()); err != nil {
+ return activator.ProbeResult{}, err
+ }
+ }
+ if err := s.Reserve(ctx, 0); err != nil {
+ return activator.ProbeResult{}, err
+ }
+ if err := s.Materialize(ctx); err != nil {
+ return activator.ProbeResult{}, err
+ }
+ p.last = activator.ProbeResult{Session: ah.Session, Stamp: ah.Stamp, Generation: ah.Generation}
+ p.live = true
+ return p.last, nil
+}
+
+// liveProbe is the activator's LiveProbe over the mode's last passing call.
+func (p *prodProbe) liveProbe(host string) (string, activator.ProbeResult, bool) {
+ if host != p1Host || !p.live {
+ return "", activator.ProbeResult{}, false
+ }
+ return p1ID, p.last, true
+}
diff --git a/internal/host/movesphase1_harness_test.go b/internal/host/movesphase1_harness_test.go
index c9155067f..3f3583f64 100644
--- a/internal/host/movesphase1_harness_test.go
+++ b/internal/host/movesphase1_harness_test.go
@@ -16,6 +16,7 @@ import (
"time"
"github.com/carlosframework/platform/internal/appconfig"
+ "github.com/carlosframework/platform/internal/hostbox"
"github.com/carlosframework/platform/internal/hostclaim"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/moves"
@@ -355,7 +356,60 @@ type phase1Harness struct {
// src is the source box's placement code: "ie", or the home's "" for a
// move out of home (fromHome).
src string
-}
+ // The source box, its data bucket and the bucket's AWS region: ie-box,
+ // data-ie and eu-west-1 by default.
+ srcBox, srcBucket, srcAWS string
+ // dst is the target box's placement code: "uk", or the home's "" for a
+ // move into home (intoHome). dstBox, dstBucket and dstAWS name the
+ // target box, its data bucket and the bucket's AWS region.
+ dst, dstBox, dstBucket, dstAWS string
+ // dstEngine is the target box's claim engine region, its roster name:
+ // "uk", or the home box's roster name "ie" for a move into home (claim
+ // Plan 13 keeps the roster name as a home box's engine region and grants
+ // it the @home alias).
+ dstEngine string
+ // homeRegion is the fleet's signed home designation, "" when the
+ // fixture names none. With it the claim's op carries home_region and the
+ // target's ParkHomeRegion is set, as armMovesParks sets it.
+ homeRegion string
+ // admitted starts the move as the claim-mode door does
+ // (hostclaim.BeginAdmittedMove with the roster's enrollments), not the
+ // plain BeginMove.
+ admitted bool
+ // roster is the signed roster the fixture's boxes resolve through.
+ roster p1Roster
+ // retain is the claim state seeded at start as an earlier stay left it
+ // (retainTouched, retainQuiet, retainChore).
+ retain p1Retained
+}
+
+// p1Roster is the fixture's signed roster: each box's actor, as
+// hostbox.Roster answers it.
+type p1Roster map[string]hostbox.Actor
+
+func (r p1Roster) Actor(_ context.Context, box string) (hostbox.Actor, error) {
+ a, ok := r[box]
+ if !ok {
+ return hostbox.Actor{}, hostbox.ErrChanged
+ }
+ return a, nil
+}
+
+// p1Retained is claim state an earlier stay left, seeded at start in
+// whichever spelling of home the option names: touched.regions, a
+// quiet_after key, and an earlier residue chore owed in the home region.
+type p1Retained struct {
+ touched, quiet, chore string
+}
+
+// retainedChoreOp is the earlier op whose residue chore retainChore seeds.
+const retainedChoreOp = "mv_earlierstayearlierstayearlie"
+
+// The fixture's enrollments (the roster's 32-hex tokens).
+const (
+ p1SrcEnrollment = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"
+ p1DstEnrollment = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2"
+)
// p1Option adjusts a Phase 1 harness before it is built.
type p1Option func(*phase1Harness)
@@ -365,13 +419,54 @@ type p1Option func(*phase1Harness)
// parks, part 1). The source box keeps its name and bucket.
func fromHome(h *phase1Harness) { h.src = "" }
+// intoHome builds the harness for a move into the home placement, the
+// mirror of fromHome for the target (home part 2). The source is placed in a
+// named code that is not the home's name (ca-box in ca, bucket data-ca), so
+// no fixture name suggests home. The target is home-box: placement "",
+// bucket data-home in eu-west-1, an edges file with a data bucket and no
+// placement, and a held environment with no CARLOS_REGION. The roster names
+// home-box in region ie and designates ie the fleet's home; the claim's op
+// carries home_region ie (BeginAdmittedMove), and the target's
+// ParkHomeRegion is ie, as armMovesParks sets it.
+func intoHome(h *phase1Harness) {
+ h.src, h.srcBox, h.srcBucket, h.srcAWS = "ca", "ca-box", "data-ca", "ca-central-1"
+ h.dst, h.dstBox, h.dstBucket, h.dstAWS, h.dstEngine = "", "home-box", "data-home", "eu-west-1", "ie"
+ h.homeRegion, h.admitted = "ie", true
+}
+
+// retainTouched seeds touched.regions with region, an earlier stay's
+// spelling of home ("ie" or "@home").
+func retainTouched(region string) p1Option {
+ return func(h *phase1Harness) { h.retain.touched = region }
+}
+
+// retainQuiet seeds a past quiet_after key under region, either spelling.
+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.
+func retainChore(region string) p1Option { return func(h *phase1Harness) { h.retain.chore = region } }
+
func newPhase1Harness(t *testing.T, opts ...p1Option) *phase1Harness {
t.Helper()
- h := &phase1Harness{moveHarness: newMoveHarness(t), lease: &fakeStagingLease{}, clock: &stepClock{now: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)}, src: "ie"}
+ h := &phase1Harness{moveHarness: newMoveHarness(t), lease: &fakeStagingLease{}, clock: &stepClock{now: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC)}, src: "ie",
+ srcBox: "ie-box", srcBucket: "data-ie", srcAWS: "eu-west-1", dst: "uk", dstBox: "uk-box", dstBucket: "data-uk", dstAWS: "eu-west-2", dstEngine: "uk"}
for _, o := range opts {
o(h)
}
h.cfg.Region = h.src
+ h.cfg.OwnBucket, h.cfg.OwnBucketRegion = h.srcBucket, h.srcAWS
+ h.roster = p1Roster{
+ h.srcBox: {Box: h.srcBox, Enrollment: p1SrcEnrollment, Region: park.PlacementRegion(h.src), FleetHomeRegion: h.homeRegion, Bucket: h.srcBucket, Eligible: true},
+ h.dstBox: {Box: h.dstBox, Enrollment: p1DstEnrollment, Region: h.dstEngine, FleetHomeRegion: h.homeRegion, Bucket: h.dstBucket, Eligible: true},
+ }
+ if h.dst == "" {
+ // The home box's roster entry equals the fleet's home designation:
+ // the claim grants it the @home alias (claim Plan 13).
+ a := h.roster[h.dstBox]
+ a.HomeRegion = h.homeRegion
+ h.roster[h.dstBox] = a
+ }
h.vs = &fakeVersioned{f: h.f}
h.lease.until = h.clock.now.Add(moves.StagingLeaseTTL)
inc, err := hostclaim.Incarnation("bab", "app", p1Host, p1Created)
@@ -395,7 +490,7 @@ func newPhase1Harness(t *testing.T, opts ...p1Option) *phase1Harness {
ctx := context.Background()
h.cfg.Enabled = true
- h.cfg.Box = "ie-box"
+ h.cfg.Box = h.srcBox
h.cfg.Now = h.clock.Now
h.cfg.OptDir = h.optDir
h.cfg.DeployBucket = "deploy"
@@ -445,19 +540,34 @@ func newPhase1Harness(t *testing.T, opts ...p1Option) *phase1Harness {
}
pointLiveForTest(t, h.optDir, p1Host, "v7")
loc := "instances/" + p1Host + "/" + inc + "/"
- h.f.store("data-ie", loc+"g-cur/0000/0000000000000001-0000000000000002.ltx",
+ h.f.store(h.srcBucket, loc+"g-cur/0000/0000000000000001-0000000000000002.ltx",
fakeMoveObject{lease.MirrorObject{ETag: "cur-a", Size: 2 << 20}, string(ltxForTest(t, 4096, 4, 1, 2))})
- h.f.store("data-ie", loc+"g-cur/0001/0000000000000001-0000000000000002.ltx",
+ h.f.store(h.srcBucket, loc+"g-cur/0001/0000000000000001-0000000000000002.ltx",
fakeMoveObject{lease.MirrorObject{ETag: "cur-b", Size: 4096}, string(ltxForTest(t, 4096, 4, 1, 2))})
- h.f.putSized("data-ie", loc+"g-old/0000/0000000000000001-0000000000000001.ltx", "old-a", 4096)
- h.f.put("data-ie", loc+"generation.json", "gen")
- h.f.put("data-ie", loc+"g-cur/lock.json", "lock")
+ h.f.putSized(h.srcBucket, loc+"g-old/0000/0000000000000001-0000000000000001.ltx", "old-a", 4096)
+ h.f.put(h.srcBucket, loc+"generation.json", "gen")
+ h.f.put(h.srcBucket, loc+"g-cur/lock.json", "lock")
// The source's edges file: ie publishes its data bucket too (T10).
- if _, err := h.s.Put(ctx, "control/edges/ie-box.json",
- []byte(`{"v":1,"box":"ie-box","eip":"1.2.3.5","placement":"`+h.src+`","data_bucket":"data-ie","data_bucket_region":"eu-west-1","hosts":{}}`), ""); err != nil {
+ if _, err := h.s.Put(ctx, "control/edges/"+h.srcBox+".json",
+ []byte(`{"v":1,"box":"`+h.srcBox+`","eip":"1.2.3.5","placement":"`+h.src+`","data_bucket":"`+h.srcBucket+`","data_bucket_region":"`+h.srcAWS+`","hosts":{}}`), ""); err != nil {
t.Fatal(err)
}
+ // The target's edges file, when it is not newMoveHarness's uk-box: a
+ // home box publishes its data bucket and no placement.
+ if h.dstBox != "uk-box" {
+ if err := h.s.Delete(ctx, "control/edges/uk-box.json"); err != nil {
+ t.Fatal(err)
+ }
+ placement := ""
+ if h.dst != "" {
+ placement = `"placement":"` + h.dst + `",`
+ }
+ if _, err := h.s.Put(ctx, "control/edges/"+h.dstBox+".json",
+ []byte(`{"v":1,"box":"`+h.dstBox+`","eip":"1.2.3.4",`+placement+`"data_bucket":"`+h.dstBucket+`","data_bucket_region":"`+h.dstAWS+`","hosts":{}}`), ""); err != nil {
+ t.Fatal(err)
+ }
+ }
// The app's instances manifest: it declares no readiness path.
if err := provision.PutManifest(ctx, h.s, "bab", "app", provision.Manifest{Schema: 1}); err != nil {
t.Fatal(err)
@@ -489,7 +599,11 @@ func pointLiveForTest(t *testing.T, optDir, host, v string) {
// start writes the record (MoveStart'ed, id p1ID, cut mode cut) and the
// claim the door's CAS leaves: serving under exec ie-box, then moving with
-// target uk-box. The claim has touched ie and ie-box only.
+// target uk-box. The claim has touched ie and ie-box only. Under intoHome
+// the source is ca-box, the target home-box and the move's region the
+// placement-derived @home, begun with the admission as the claim-mode door
+// begins it (BeginAdmittedMove, home_region ie). The retained options add
+// the claim state an earlier stay left.
func (h *phase1Harness) start(t *testing.T, cut string) provision.Record {
t.Helper()
ctx := context.Background()
@@ -498,22 +612,44 @@ func (h *phase1Harness) start(t *testing.T, cut string) provision.Record {
t.Fatal(err)
}
if _, err := provision.SetRecordMove(ctx, h.s, "bab", "app", p1Host, provision.MoveStartTransition(provision.MoveStart{
- ID: p1ID, Incarnation: h.inc, From: h.src, To: "uk", Cut: cut, By: "op", Now: h.clock.Now()})); err != nil {
+ ID: p1ID, Incarnation: h.inc, From: h.src, To: h.dst, Cut: cut, By: "op", Now: h.clock.Now()})); err != nil {
t.Fatal(err)
}
obs, err := hostclaim.Update(ctx, h.s, hostclaim.Observed{Host: p1Host, Absent: true}, func(c *hostclaim.Claim) error {
c.Holder = &hostclaim.Holder{Family: "instance", Key: provision.RecordKey("bab", "app", p1Host), Account: "bab", App: "app",
Incarnation: h.inc, CreatedAt: p1Created, Location: "instances/" + p1Host + "/" + h.inc, Generation: "cur", Stamp: 1}
- c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "ie-box"}
- c.Touched = hostclaim.Touched{Complete: true, Regions: []string{park.PlacementRegion(h.src)}, Boxes: []string{"ie-box"}}
+ c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: h.srcBox}
+ c.Touched = hostclaim.Touched{Complete: true, Regions: []string{park.PlacementRegion(h.src)}, Boxes: []string{h.srcBox}}
+ if h.retain.touched != "" {
+ c.Touched.Regions = append(c.Touched.Regions, h.retain.touched)
+ }
+ if h.retain.quiet != "" {
+ c.QuietAfter = map[string]time.Time{h.retain.quiet: h.clock.Now().Add(-time.Hour).UTC()}
+ }
return nil
})
if err != nil {
t.Fatal(err)
}
- if _, err := hostclaim.Update(ctx, h.s, obs, hostclaim.BeginMove(p1ID, "op", "ie-box", "uk-box", "uk", h.clock.Now())); err != nil {
+ to := park.PlacementRegion(h.dst)
+ begin := hostclaim.BeginMove(p1ID, "op", h.srcBox, h.dstBox, to, h.clock.Now())
+ if h.admitted {
+ begin = hostclaim.BeginAdmittedMove(p1ID, "op", h.srcBox, h.dstBox, to, h.clock.Now(), hostclaim.MoveAdmission{HomeRegion: h.homeRegion,
+ AppID: "app_" + p1Host, AdmissionID: p1ID, SourceEnrollment: h.roster[h.srcBox].Enrollment, TargetEnrollment: h.roster[h.dstBox].Enrollment})
+ }
+ if _, err := hostclaim.Update(ctx, h.s, obs, begin); err != nil {
t.Fatal(err)
}
+ 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.
+ 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}})
+ })
+ }
return h.read(t, p1Host)
}
@@ -544,10 +680,15 @@ func (h *phase1Harness) w() *movesWorker {
}
// targetCfg is the uk box's configuration over the same store and buckets,
-// its hooks stubbed to pass (each test turns one off).
+// its hooks stubbed to pass (each test turns one off). Under intoHome it is
+// home-box's: placement "", ParkHomeRegion ie, and a held environment with
+// no CARLOS_REGION.
func (h *phase1Harness) targetCfg() moveCfg {
c := h.cfg
- c.Region, c.Box, c.OwnBucket, c.OwnBucketRegion = "uk", "uk-box", "data-uk", "eu-west-2"
+ c.Region, c.Box, c.OwnBucket, c.OwnBucketRegion = h.dst, h.dstBox, h.dstBucket, h.dstAWS
+ if h.dst == "" {
+ c.ParkHomeRegion = h.homeRegion
+ }
c.OwnsRoute = func(string) bool { return false }
c.Route = func(string) (*registry.Route, error) { return nil, nil }
c.ComposeHeldEnv = func(context.Context, release.Store, provision.Record, []byte) (map[string]string, []string, provision.MoveInputs, error) {
@@ -555,7 +696,7 @@ func (h *phase1Harness) targetCfg() moveCfg {
for k, v := range h.env {
env[k] = v
}
- env["CARLOS_REGION"] = "uk"
+ setHeldRegion(env, h.dst)
return env, nil, provision.MoveInputs{BundleETags: map[string]string{"default": "e1", "environment": ""}}, nil
}
c.ProbeStore = func(context.Context, map[string]string, string) (string, error) { return "", nil }
@@ -577,6 +718,17 @@ func (h *phase1Harness) targetCfg() moveCfg {
return c
}
+// setHeldRegion is the agent's rule for the target's held environment
+// (agentOwnedVars): CARLOS_REGION is the box's placement when it has one,
+// and absent on a home box.
+func setHeldRegion(env map[string]string, placement string) {
+ if placement == "" {
+ delete(env, "CARLOS_REGION")
+ return
+ }
+ env["CARLOS_REGION"] = placement
+}
+
// targetPass runs one pass as the target box.
func (h *phase1Harness) targetPass(t *testing.T, c moveCfg) {
t.Helper()
@@ -609,7 +761,7 @@ func (h *phase1Harness) sourceReport(t *testing.T) *moves.SourceReport {
func (h *phase1Harness) ledger(t *testing.T) moveLedger {
t.Helper()
- l, err := readMoveLedger(context.Background(), h.f, "data-ie", moves.LedgerKey(p1Host, p1ID))
+ l, err := readMoveLedger(context.Background(), h.f, h.srcBucket, moves.LedgerKey(p1Host, p1ID))
if err != nil {
t.Fatal(err)
}
diff --git a/internal/host/movesprobeowners_test.go b/internal/host/movesprobeowners_test.go
index 648e54ff3..e13852d73 100644
--- a/internal/host/movesprobeowners_test.go
+++ b/internal/host/movesprobeowners_test.go
@@ -25,6 +25,7 @@ import (
"github.com/carlosframework/platform/internal/lease/leasetest"
"github.com/carlosframework/platform/internal/moves"
"github.com/carlosframework/platform/internal/moves/archive"
+ "github.com/carlosframework/platform/internal/park"
"github.com/carlosframework/platform/internal/provision"
"github.com/carlosframework/platform/internal/registry"
"github.com/carlosframework/platform/internal/release"
@@ -71,6 +72,32 @@ type poFixture struct {
inc string
snapshotVersion string
sourceGeneration string
+ // The move's sides (poSides): the source box and its engine region, the
+ // target box, its engine region (its roster name) and its placement
+ // code, the claim's intent "to" and the fleet's home designation.
+ poSides
+}
+
+// poSides names the fixture's two boxes. The default is ie-box in ie to
+// uk-box in uk; poIntoHome is a move from ca-box in ca into the home
+// region, whose box home-box has the roster name ie, the record code "" and
+// the claim's to @home (home part 2).
+type poSides struct {
+ srcBox, srcRegion string
+ dstBox, dstEngine, dstCode, dstTo string
+ home string
+}
+
+var poNamed = poSides{srcBox: "ie-box", srcRegion: "ie", dstBox: "uk-box", dstEngine: "uk", dstCode: "uk", dstTo: "uk"}
+
+// poOption adjusts the fixture's sides before it is built.
+type poOption func(*poSides)
+
+// poIntoHome is a move from ca-box into the home region: the target is
+// home-box, its roster name ie, the record's code "" and the claim's to the
+// placement-derived @home, with the fleet's home designation ie.
+func poIntoHome(s *poSides) {
+ *s = poSides{srcBox: "ca-box", srcRegion: "ca", dstBox: "home-box", dstEngine: "ie", dstCode: "", dstTo: park.PlacementRegion(""), home: "ie"}
}
// poFlipClaim is the claim as the flip leaves it (moves §7.5): landing at
@@ -79,17 +106,21 @@ type poFixture struct {
func (f *poFixture) poFlipClaim() *hostclaim.Claim {
f.t.Helper()
journal := map[string]string{hostclaim.JournalSeal: f.seal.Encode(), hostclaim.JournalCutSession: "15", hostclaim.JournalEvidenceSource: "moves/bab/jam/" + poHost + "/" + poOp + "/stopped-source.json", hostclaim.JournalSourceStamp: "15", hostclaim.JournalSrcGen: "src"}
+ intent := map[string]string{"to": f.dstTo, "source": f.srcBox, "target": f.dstBox}
+ if f.home != "" {
+ intent["home_region"] = f.home
+ }
c := &hostclaim.Claim{V: 2, Host: poHost, Gen: 19,
- Holder: &hostclaim.Holder{Family: "instance", Key: "apps/bab/jam/instances/" + poHost + ".json", Account: "bab", App: "jam", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), Incarnation: f.inc, Location: "instances/" + poHost, Generation: "src", Stamp: 18, Session: 15, WriterBox: "ie-box",
- Admissions: []hostclaim.AdmissionRecord{{Session: 15, Box: "ie-box", Generation: "src", Predecessor: "", Region: "ie", Mode: "cold"}}},
- Op: hostclaim.Op{Kind: hostclaim.KindLanding, ID: poOp, Phase: hostclaim.PhaseLandingProbing, Exec: "uk-box", Participants: []string{"ie-box", "uk-box"}, Intent: map[string]string{"to": "uk", "source": "ie-box", "target": "uk-box"}, Journal: journal}}
+ Holder: &hostclaim.Holder{Family: "instance", Key: "apps/bab/jam/instances/" + poHost + ".json", Account: "bab", App: "jam", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), Incarnation: f.inc, Location: "instances/" + poHost, Generation: "src", Stamp: 18, Session: 15, WriterBox: f.srcBox,
+ Admissions: []hostclaim.AdmissionRecord{{Session: 15, Box: f.srcBox, Generation: "src", Predecessor: "", Region: f.srcRegion, Mode: "cold"}}},
+ Op: hostclaim.Op{Kind: hostclaim.KindLanding, ID: poOp, Phase: hostclaim.PhaseLandingProbing, Exec: f.dstBox, Participants: []string{f.srcBox, f.dstBox}, Intent: intent, Journal: journal}}
if err := c.Validate(); err != nil {
f.t.Fatal(err)
}
return c
}
-func newPOFixture(t *testing.T) *poFixture {
+func newPOFixture(t *testing.T, opts ...poOption) *poFixture {
t.Helper()
// Claim Plan 15: admissions through the real CAS record their origin
// under both switches; the real C8's proof reads that record.
@@ -100,7 +131,10 @@ func newPOFixture(t *testing.T) *poFixture {
if err != nil {
t.Fatal(err)
}
- f := &poFixture{t: t, claims: release.NewDirStore(t.TempDir()), bucket: leasetest.NewVersionedBucket(), dataDir: t.TempDir(), files: t.TempDir(), inc: inc}
+ f := &poFixture{t: t, claims: release.NewDirStore(t.TempDir()), bucket: leasetest.NewVersionedBucket(), dataDir: t.TempDir(), files: t.TempDir(), inc: inc, poSides: poNamed}
+ for _, o := range opts {
+ o(&f.poSides)
+ }
f.bucket.AddBucket("data")
f.seal = moves.Seal{V: moves.SealVersion, N: 42, FromDB: true, SrcCovered: true, SealedAt: at, LeaseUntil: at.Add(time.Minute),
Replica: []moves.SealedObject{{Key: "moves/" + poHost + "/" + poOp + "/replica/0009/" + ltx.FormatFilename(1, 42), VersionID: "v1", Size: int64(ltx.HeaderSize)}},
@@ -149,14 +183,14 @@ func newPOFixture(t *testing.T) *poFixture {
pins := mpPins()
pins.EnvReport = "sha256:env"
deadline := time.Date(2026, 10, 9, 12, 10, 0, 0, time.UTC)
- f.rec = &provision.Record{Host: poHost, Account: "bab", App: "jam", Region: "uk",
- Move: &provision.MoveState{ID: poOp, Incarnation: inc, From: "ie", To: "uk", State: provision.MoveLanding, LandingPhase: provision.LandProbing, Inputs: pins, CutDeadline: &deadline,
+ f.rec = &provision.Record{Host: poHost, Account: "bab", App: "jam", Region: f.dstCode,
+ Move: &provision.MoveState{ID: poOp, Incarnation: inc, From: f.srcRegion, To: f.dstCode, State: provision.MoveLanding, LandingPhase: provision.LandProbing, Inputs: pins, CutDeadline: &deadline,
Landing: &provision.MoveLandingJournal{Attempts: 1, InstalledDBSHA256: "sha256:" + f.manifest.DBSHA256}}}
f.launch = ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:snapshot", Release: "sha256:bytes"}, Executable: "/opt/carlos/live/" + poHost, ExecutableSHA256: strings.Repeat("b", 64), Args: []string{"--socket", f.route.Socket, "--db", f.route.DBPath}, Env: []string{"A=1", "CARLOS_MOVE_PROBE=0"}, Version: pins.Release}
pins.EnvReport = envDigest(poEnvKey, f.launch.Env)
f.admitted = 3
f.renderETags, f.renderOverlay = pins.BundleETags, pins.OverlayETag
- f.owners = &movesProbeOwners{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: "uk", Box: "uk-box", DataDir: f.dataDir,
+ f.owners = &movesProbeOwners{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: f.dstEngine, Box: f.dstBox, DataDir: f.dataDir,
Records: func(context.Context, string, string, string) (*provision.Record, error) { f.reads++; return f.rec, nil },
ReadyPath: func(context.Context, string, string) (string, error) { return "/healthz", nil },
FilesDir: func(string) (string, string) { return f.files, filepath.Join(f.dataDir, poHost+".env") },
@@ -189,6 +223,10 @@ func newPOFixture(t *testing.T) *poFixture {
},
NewID: func() string { f.seq++; return fmt.Sprintf("fresh%d", f.seq) },
Now: func() time.Time { return time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC) }}
+ if f.home != "" {
+ // The target is the home box: the roster grants it the alias.
+ f.owners.HomeRegion, f.owners.FleetHomeRegion = f.home, f.home
+ }
// The C8 provider is the real one over the fixture's bucket, as the
// claim assembly binds it (whole-branch fixes, item 1).
f.owners.Files = f.realC8()
diff --git a/internal/host/movesrecoveryowners_test.go b/internal/host/movesrecoveryowners_test.go
index b7f30518f..69d597998 100644
--- a/internal/host/movesrecoveryowners_test.go
+++ b/internal/host/movesrecoveryowners_test.go
@@ -74,11 +74,14 @@ func rcInputs(env []string) *provision.MoveInputs {
return &provision.MoveInputs{BundleETags: map[string]string{"default": "e-recovered"}, OverlayETag: "e-overlay-2", Release: "v1.3.0", EnvReport: envDigest(poEnvKey, env)}
}
-func newRCFixture(t *testing.T, kind string) *rcFixture {
+func newRCFixture(t *testing.T, kind string, opts ...poOption) *rcFixture {
t.Helper()
- po := newPOFixture(t)
+ po := newPOFixture(t, opts...)
f := &rcFixture{poFixture: po, kind: kind, c8: &rcFiles{f: po}}
- f.owners.Box, f.owners.Region = "ie-box", "ie"
+ // The recovery runs on the source box, which is never the home box
+ // here: no alias, the fleet's designation only.
+ f.owners.Box, f.owners.Region = f.srcBox, f.srcRegion
+ f.owners.HomeRegion, f.owners.FleetHomeRegion = "", f.home
f.owners.Files = f.c8
f.launch.Version = "v1.3.0"
f.inputs = rcInputs(f.launch.Env)
@@ -90,11 +93,11 @@ func newRCFixture(t *testing.T, kind string) *rcFixture {
f.bucket.Put("data", "instances/"+poHost+"/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30))
} else {
f.bucket.Put("data", "instances/"+poHost+"/g-src/0009/"+ltx.FormatFilename(1, 30), f.ltxHeader(1, 30))
- f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: "ie-box", InstalledAt: at}))
+ f.bucket.Put("data", "instances/"+poHost+"/generation.json", mustJSON(t, genselect.Marker{Op: "src", Incarnation: f.inc, Host: poHost, Box: f.srcBox, InstalledAt: at}))
}
c := f.recoveryClaim()
- f.rec.Region = "ie"
+ f.rec.Region = f.srcRegion
f.rec.Move.LandingPhase = provision.LandRestored
f.route.Status, f.route.HoldOp = "restoring", rpRB
switch kind {
@@ -175,7 +178,7 @@ func newRCFixture(t *testing.T, kind string) *rcFixture {
// Plan 15: the source generation the clean kinds bridge from is proven
// only when the claim records it; record it as the #250 backfill does.
if kind != rcLegacy {
- recordOrigins(t, f.claims, poHost, hostclaim.OriginTuple{Incarnation: f.inc, Location: "instances/" + poHost, Region: "ie", Generation: "src"})
+ recordOrigins(t, f.claims, poHost, hostclaim.OriginTuple{Incarnation: f.inc, Location: "instances/" + poHost, Region: f.srcRegion, Generation: "src"})
}
return f
}
@@ -187,12 +190,16 @@ func newRCFixture(t *testing.T, kind string) *rcFixture {
// left the source.
func (f *rcFixture) recoveryClaim() *hostclaim.Claim {
h := &hostclaim.Holder{Family: "instance", Key: "apps/bab/jam/instances/" + poHost + ".json", Account: "bab", App: "jam", CreatedAt: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), Incarnation: f.inc, Location: "instances/" + poHost,
- Generation: poOp, Stamp: rcS, Session: 20, WriterBox: "uk-box",
- Admissions: []hostclaim.AdmissionRecord{{Session: 15, Box: "ie-box", Generation: "src", Region: "ie", Mode: "cold"},
- {Session: 20, Box: "uk-box", Generation: poOp, Predecessor: "src", Region: "uk", PredecessorRegion: "ie", Mode: "cold", Coverage: "sealed"}}}
+ Generation: poOp, Stamp: rcS, Session: 20, WriterBox: f.dstBox,
+ Admissions: []hostclaim.AdmissionRecord{{Session: 15, Box: f.srcBox, Generation: "src", Region: f.srcRegion, Mode: "cold"},
+ {Session: 20, Box: f.dstBox, Generation: poOp, Predecessor: "src", Region: f.dstEngine, PredecessorRegion: f.srcRegion, Mode: "cold", Coverage: "sealed"}}}
journal := map[string]string{hostclaim.JournalSourceStamp: "15", hostclaim.JournalSrcGen: "src", hostclaim.JournalCutSession: "15", hostclaim.JournalEvidenceSource: "moves/bab/jam/" + poHost + "/" + poOp + "/stopped-source.json"}
- op := hostclaim.Op{Kind: hostclaim.KindRestoring, ID: poOp, Phase: hostclaim.PhaseProbing, Exec: "ie-box", Participants: []string{"ie-box", "uk-box"},
- Intent: map[string]string{"to": "uk", "source": "ie-box", "target": "uk-box", "rollback": rpRB}, Journal: journal}
+ intent := map[string]string{"to": f.dstTo, "source": f.srcBox, "target": f.dstBox, "rollback": rpRB}
+ if f.home != "" {
+ intent["home_region"] = f.home
+ }
+ op := hostclaim.Op{Kind: hostclaim.KindRestoring, ID: poOp, Phase: hostclaim.PhaseProbing, Exec: f.srcBox, Participants: []string{f.srcBox, f.dstBox},
+ Intent: intent, Journal: journal}
gen := uint64(30)
switch f.kind {
case rcClean:
@@ -204,7 +211,7 @@ func (f *rcFixture) recoveryClaim() *hostclaim.Claim {
h.Admissions[0].Predecessor = ""
journal[hostclaim.JournalSrcGen] = ""
case rcRevert:
- h.Generation, h.Session, h.WriterBox, h.Admissions = "src", 15, "ie-box", h.Admissions[:1]
+ h.Generation, h.Session, h.WriterBox, h.Admissions = "src", 15, f.srcBox, h.Admissions[:1]
op.Kind = hostclaim.KindReverting
delete(op.Intent, "rollback")
journal[hostclaim.JournalRevPhase] = hostclaim.PhaseProbing
@@ -1042,7 +1049,7 @@ func TestRecoveryStartGateRefusesATreeChangedBeforeTheFirstStart(t *testing.T) {
// the owners' AdmissionEvidence, then the CAS.
func (f *rcFixture) admitThroughWriter(o hostclaim.Observed, in ClaimLaunchInputs) hostclaim.Observed {
f.t.Helper()
- w := hostbox.Writer{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: "ie", Box: "ie-box", AdmissionEvidence: f.owners.AdmissionEvidence}
+ w := hostbox.Writer{Claims: f.claims, Data: f.bucket, Bucket: "data", Region: f.srcRegion, Box: f.srcBox, AdmissionEvidence: f.owners.AdmissionEvidence}
admitted, err := w.Admit(f.t.Context(), o, in.Admission, in.Baseline)
if err != nil {
f.t.Fatal(err)
diff --git a/internal/host/movesrevert_test.go b/internal/host/movesrevert_test.go
index 9731834d4..dc2c99ddf 100644
--- a/internal/host/movesrevert_test.go
+++ b/internal/host/movesrevert_test.go
@@ -48,9 +48,9 @@ type revertHarness struct {
rvHits []string
}
-func newRevertHarness(t *testing.T) *revertHarness {
+func newRevertHarness(t *testing.T, opts ...p1Option) *revertHarness {
t.Helper()
- h := &revertHarness{cutHarness: newCutHarness(t), tdir: t.TempDir(), tleases: &fakeParkLeases{}}
+ h := &revertHarness{cutHarness: newCutHarness(t, opts...), tdir: t.TempDir(), tleases: &fakeParkLeases{}}
// The row follows the cut's hold, as holdMoving keeps it: held moving
// under the op, a row held by another op refused.
h.cfg.HoldMoving = func(host, op string) error {
@@ -81,7 +81,7 @@ func (h *revertHarness) fromStopping(t *testing.T) {
if m.State != provision.MoveReverting || m.CutPhase != provision.CutStopping || m.Source != nil || m.Reason != provision.RevertStopEvidenceBudget {
t.Fatalf("the fixture did not revert at stopping: %+v", m)
}
- if c := h.claim(t); c.Op.Kind != hostclaim.KindMoving || c.Op.Exec != "ie-box" {
+ if c := h.claim(t); c.Op.Kind != hostclaim.KindMoving || c.Op.Exec != h.srcBox {
t.Fatalf("the fixture's claim: %+v", c.Op)
}
if h.route.Status != "moving" || h.route.HoldOp != p1ID {
@@ -165,7 +165,7 @@ func revertRendered() provision.MoveInputs {
func (h *revertHarness) laneCfg() moveCfg {
c := h.target()
c.DataDir = h.tdir
- armParkFakes(&c, h.s, h.tleases, c.Region, roleTarget)
+ armParkFakes(&c, h.s, h.tleases, park.PlacementRegion(c.Region), roleTarget)
return c
}
@@ -200,7 +200,7 @@ func (h *revertHarness) revPhase(t *testing.T) string {
func (h *revertHarness) laneChore(t *testing.T) *hostclaim.Obligation {
t.Helper()
for _, o := range h.claim(t).Obligations {
- if o.Op == p1ID && o.Kind == "park-replica" && o.Region == "uk" && o.Park != nil && o.Park.Location == moves.ScopeLocation(p1Host, p1ID) {
+ if o.Op == p1ID && o.Kind == "park-replica" && o.Region == park.PlacementRegion(h.dst) && o.Park != nil && o.Park.Location == moves.ScopeLocation(p1Host, p1ID) {
return &o
}
}
@@ -210,7 +210,7 @@ func (h *revertHarness) laneChore(t *testing.T) *hostclaim.Obligation {
// laneKeys are the objects still in the move's lane in the target bucket.
func (h *revertHarness) laneKeys() []string {
var out []string
- for k := range h.f.buckets["data-uk"] {
+ for k := range h.f.buckets[h.dstBucket] {
if strings.HasPrefix(k, moves.ScopeLocation(p1Host, p1ID)+"/") {
out = append(out, k)
}
@@ -236,7 +236,7 @@ func (h *revertHarness) reopened(t *testing.T) {
t.Fatalf("the source did not reopen: %+v", m)
}
c := h.claim(t)
- if c.Op.Kind != hostclaim.KindReverting || c.Op.Phase != hostclaim.PhaseOpened || c.Op.Exec != "ie-box" || c.Op.Journal[hostclaim.JournalRevPhase] != hostclaim.PhaseOpened {
+ if c.Op.Kind != hostclaim.KindReverting || c.Op.Phase != hostclaim.PhaseOpened || c.Op.Exec != h.srcBox || c.Op.Journal[hostclaim.JournalRevPhase] != hostclaim.PhaseOpened {
t.Fatalf("the claim after the reopen: %+v", c.Op)
}
if h.route.HoldOp != "" || h.route.Status != "ready" {
@@ -269,11 +269,11 @@ func (h *revertHarness) drive(t *testing.T) {
func (h *revertHarness) closed(t *testing.T) {
t.Helper()
rec := h.read(t, p1Host)
- if rec.Move != nil || rec.Region != "ie" || rec.LastMove == nil || rec.LastMove.ID != p1ID || rec.LastMove.Outcome != "rolled_back" {
+ if rec.Move != nil || rec.Region != h.src || rec.LastMove == nil || rec.LastMove.ID != p1ID || rec.LastMove.Outcome != "rolled_back" {
t.Fatalf("the record after the close: region %s move %+v last %+v", rec.Region, rec.Move, rec.LastMove)
}
c := h.claim(t)
- if c.Op.Kind != hostclaim.KindServing || c.Op.Exec != "ie-box" || hostclaim.RequireReleasedMove(p1ID)(c) != nil {
+ if c.Op.Kind != hostclaim.KindServing || c.Op.Exec != h.srcBox || hostclaim.RequireReleasedMove(p1ID)(c) != nil {
t.Fatalf("the claim after the close: %+v", c.Op)
}
if h.route.HoldOp != "" || h.route.Status != "ready" {