| index a18504b56..b8312514a 100644 |
| --- a/internal/host/relocationwake_test.go |
| +++ b/internal/host/relocationwake_test.go |
| @@ -24,18 +24,34 @@ import ( |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| |
| -// Finding 6, ruling 47: P's first probe publishes through the shared |
| -// assembly. The claim's own runtime (the recovery fixture's controller) |
| -// admits P's opening through the relocation dispatch, binds region HA's |
| -// attempt journal to the probe start, and Materialize, under the admitted |
| -// spawn selection, runs moves' C8 Establish and writes P's marker: P as |
| -// the marker's op, the install manifest's pin, prepared_authority's |
| -// digest. Publish (the pre-replication call) finds that marker and |
| -// records the receipt; PublicationEvidence then passes. Publish without |
| -// Materialize's fence refuses. |
| -func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) { |
| - f := newRecoveryFixture(t) |
| +// relocationProbeRig is P's first probe on box-1 through the claim's own |
| +// runtime (the recovery fixture's controller), the relocation owners bound |
| +// as claimGroupDWiring binds them, the probe attempt counted and pending. |
| +type relocationProbeRig struct { |
| + f *recoveryFixture |
| + bucket *leasetest.VersionedBucket |
| + files *release.DirStore |
| + c8 *roFiles |
| + inputs *RelocationWakeInputs |
| + c *recoveryController |
| + h *hostclaim.Holder |
| + o hostclaim.Observed |
| + im moves.InstallManifest |
| + auth hostclaim.PreparedAuthority |
| + route registry.Route |
| +} |
| + |
| +// roProbeLaunch is the serving launch source's result for the rig: the |
| +// live link hashed as build v1, staged under v1. |
| +func roProbeLaunch() ClaimServingLaunch { |
| + return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Version: "v1", |
| + Staged: release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/bab/jam", Digest: strings.Repeat("a", 64)}} |
| +} |
| + |
| +func newRelocationProbeRig(t *testing.T, launch ClaimServingLaunch) *relocationProbeRig { |
| + t.Helper() |
| ctx := t.Context() |
| + f := newRecoveryFixture(t) |
| bucket := leasetest.NewVersionedBucket() |
| bucket.AddBucket("data") |
| files := release.NewDirStore(t.TempDir()) |
| @@ -72,9 +88,7 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) { |
| } |
| c8 := &roFiles{} |
| inputs := &RelocationWakeInputs{ |
| - Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { |
| - return ClaimServingLaunch{Identity: hostwrite.Identity{Config: "sha256:config", Release: "sha256:release"}, Executable: "/usr/bin/true", ExecutableSHA256: strings.Repeat("a", 64), Version: "v1"}, nil |
| - }, |
| + Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { return launch, nil }, |
| FilesManifest: c8, Markers: plan9D5Markers{bucket}, Claims: f.claims, FilesStore: files, Data: bucket, Bucket: "data", Region: "ie", Box: "box-1", |
| NewID: f.newID, Now: func() time.Time { return f.now }, Releases: &roReleases{}} |
| attempts := &relocationProbeAttempts{Claims: f.claims, DataDir: t.TempDir(), Box: "box-1", NewID: f.newID, Now: func() time.Time { return f.now }} |
| @@ -92,7 +106,22 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) { |
| c.wake.pending[recoveryHost] = &claimWakeAttempt{observed: o, held: recoveryHeld{}, ctx: live, cancel: cancel, done: done} |
| c.wake.mu.Unlock() |
| route := registry.Route{Host: recoveryHost, Kind: "instance", App: "jam", Account: "bab", Status: "relocating", HoldOp: roOp, Socket: filepath.Join(f.guard.DataDir, "app.sock"), DBPath: filepath.Join(f.guard.DataDir, "state", "data.db")} |
| + return &relocationProbeRig{f: f, bucket: bucket, files: files, c8: c8, inputs: inputs, c: c, h: h, o: o, im: im, auth: auth, route: route} |
| +} |
| |
| +// Finding 6, ruling 47: P's first probe publishes through the shared |
| +// assembly. The claim's own runtime (the recovery fixture's controller) |
| +// admits P's opening through the relocation dispatch, binds region HA's |
| +// attempt journal to the probe start, and Materialize, under the admitted |
| +// spawn selection, runs moves' C8 Establish and writes P's marker: P as |
| +// the marker's op, the install manifest's pin, prepared_authority's |
| +// digest. Publish (the pre-replication call) finds that marker and |
| +// records the receipt; PublicationEvidence then passes. Publish without |
| +// Materialize's fence refuses. |
| +func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) { |
| + r := newRelocationProbeRig(t, roProbeLaunch()) |
| + ctx := t.Context() |
| + f, bucket, files, c8, inputs, c, h, o, im, auth, route := r.f, r.bucket, r.files, r.c8, r.inputs, r.c, r.h, r.o, r.im, r.auth, r.route |
| admitted, err := c.runtime.Admit(ctx, route, o, roProbe) |
| if err != nil { |
| t.Fatal(err) |
| @@ -153,6 +182,60 @@ func TestRelocationFirstProbePublishesThroughTheAssembly(t *testing.T) { |
| } |
| } |
| |
| +// L18 joint round, final 4, item 1 (ruling R-EXEC-CLAIMVERSION): the |
| +// relocation probe's start names the file its launch source hashed. With |
| +// the production BeforeExec hook set, the activator refuses any claimed |
| +// start whose Staged.Version is not its Version (Service.claimedBuild, |
| +// run on the start the runtime's Prepare returns), so a probe selection |
| +// that drops Staged never starts. The start Prepare hands the activator |
| +// for P's prepared opening carries the launch's Staged, and so does every |
| +// selection: a later cold attempt's and an empty P's. |
| +func TestTheRelocationProbeStartNamesTheBuildItHashed(t *testing.T) { |
| + ctx := t.Context() |
| + launch := roProbeLaunch() |
| + r := newRelocationProbeRig(t, launch) |
| + admitted, err := r.c.runtime.Admit(ctx, r.route, r.o, roProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + s, err := r.c.runtime.Prepare(ctx, r.route, admitted, roProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + // claimedBuild's refusal, as the activator applies it with BeforeExec set. |
| + if s.Staged != launch.Staged || s.Version != launch.Version || s.Staged.Version != s.Version { |
| + t.Fatalf("the probe start names build %q but staged %+v; the launch hashed %+v", s.Version, s.Staged, launch.Staged) |
| + } |
| + |
| + for _, o := range []roOptions{{}, {empty: true}} { |
| + f := newROFixture(t, o) |
| + source := f.inputs.Launch |
| + staged := release.StagedIdentity{Version: "v1", StagedPath: "/opt/carlos/versions/v1/acct/app", Digest: strings.Repeat("b", 64)} |
| + f.inputs.Launch = func(ctx context.Context, rt registry.Route) (ClaimServingLaunch, error) { |
| + l, err := source(ctx, rt) |
| + l.Staged = staged |
| + return l, err |
| + } |
| + first, err := f.inputs.Select(ctx, f.route, f.read(), roProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if first.Staged != staged || first.Staged.Version != first.Version { |
| + t.Fatalf("empty=%v: the first attempt names build %q but staged %+v", o.empty, first.Version, first.Staged) |
| + } |
| + if o.empty { |
| + continue |
| + } |
| + again, err := f.inputs.Select(ctx, f.route, f.admit(f.read(), first), roProbe) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if again.Prepared || again.Staged != staged || again.Staged.Version != again.Version { |
| + t.Fatalf("a later attempt (prepared %v) names build %q but staged %+v", again.Prepared, again.Version, again.Staged) |
| + } |
| + } |
| +} |
| + |
| // Ruling 47: a later attempt's fresh generation, and an empty P's opening, |
| // publish through main's fenced writer only after their first root. |
| func TestRelocationLaterAttemptsPublishFenced(t *testing.T) { |