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>
11 files changed,
+804
−98
internal/host/claimretryrecovery_test.go+29 −5internal/host/movescut_test.go+6 −6internal/host/movesfinal_test.go+48 −21internal/host/moveshome_fixture_test.go+195 −0internal/host/movesland_probe_test.go+2 −2internal/host/movesland_test.go+11 −11internal/host/movesparks_test.go+267 −4internal/host/movesphase1_harness_test.go+171 −19internal/host/movesprobeowners_test.go+46 −8internal/host/movesrecoveryowners_test.go+20 −13internal/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" { |