package host import ( "context" "crypto/ed25519" "crypto/rand" "encoding/json" "errors" "os" "path/filepath" "reflect" "slices" "strings" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/boxvolumes" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostrelocate" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" "github.com/carlosframework/platform/internal/termfence" "github.com/superfly/ltx" ) // passClaim writes a claim for host into store with op as given; serving // claims have no op id. func passClaim(t *testing.T, store release.Store, host string, op hostclaim.Op) { t.Helper() ctx := t.Context() created := time.Date(2026, 10, 7, 1, 0, 0, 0, time.UTC) nh := hostclaim.NewHolder{Family: "instance", Key: "apps/bab/jam/instances/" + host + ".json", Account: "bab", App: "jam", Host: host, Region: "ie", RecordRegion: "ie", CreatedAt: created} body := []byte(`{"host":"` + host + `","account":"bab","app":"jam","region":"ie","created_at":"2026-10-07T01:00:00Z"}`) obs, err := hostclaim.Read(ctx, store, host) if err != nil { t.Fatal(err) } for _, tr := range []func(*hostclaim.Claim) error{ hostclaim.BeginCreateOn(nh, body, "c1", "console", created, hostclaim.ExecutorSelection{Box: "src", Enrollment: strings.Repeat("b", 32)}), hostclaim.CompleteCreate("c1", "src"), hostclaim.AdmitWriter(hostclaim.Admission{Box: "src", Mode: "cold", NewGeneration: "g1", Region: "ie", KeepStamp: true}), func(c *hostclaim.Claim) error { if op.Kind != "" { c.Op = op } return nil }, } { if obs, err = hostclaim.Update(ctx, store, obs, tr); err != nil { t.Fatal(err) } } } func relocatingOp(id string) hostclaim.Op { return hostclaim.Op{Kind: hostclaim.KindRelocating, ID: id, Phase: hostclaim.PhaseInstalling, Exec: "dst", Participants: []string{"src", "dst"}, Intent: map[string]string{"region": "ie", "source_box": "src", "target_box": "dst"}} } // stepRecorder is a worker or participant that records each Step. type stepRecorder struct { name string order *[]string err error } func (s stepRecorder) Step(_ context.Context, host string) (hostclaim.Observed, error) { *s.order = append(*s.order, s.name+":"+host) return hostclaim.Observed{}, s.err } // Spec ยง8 step 2 and ruling 82: nothing runs with CARLOS_HOST_CLAIM off. // With claim mode on, CARLOS_REGION_HA off, or the relocation owners not // bound, the runtime is the completion-only one: it steps nothing, // converts nothing and reads only this box's registry rows. func TestRelocationRuntimeFollowsTheFlags(t *testing.T) { reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() local := &productionClaimLocal{legacyParticipation: true, assembly: &ClaimAssembly{Guard: hostguard.Guard{DataDir: t.TempDir()}}} for _, tc := range []struct { claim, ha string completion bool }{{"", "", false}, {"", "1", false}, {"1", "", true}, {"1", "1", true}} { getenv := func(k string) string { switch k { case "CARLOS_HOST_CLAIM": return tc.claim case "CARLOS_REGION_HA": return tc.ha } return "" } r, err := NewRelocationRuntime(t.Context(), getenv, release.NewDirStore(t.TempDir()), local, ClaimProviders{}, reg, &activator.Service{}, relocationRuntimeInputs{}) if err != nil { t.Fatalf("claim %q region HA %q: %v", tc.claim, tc.ha, err) } if !tc.completion { if r != nil { t.Fatalf("claim %q region HA %q: built %+v", tc.claim, tc.ha, r) } continue } // With region HA on, the owners are not bound here (no wake // dispatcher), so this is the completion-only runtime too. if r == nil || r.Worker != nil || r.Participant != nil || r.Convert != nil || r.Finish == nil || r.Hosts == nil { t.Fatalf("claim %q region HA %q: not a completion-only runtime: %+v", tc.claim, tc.ha, r) } } } // Ruling 82: on a box with region HA off that never completed a // relocation, the completion pass writes nothing. Its hosts are its own // rows; a serving claim with no completion is left alone, and so is one // whose completion names another box. func TestCompletionPassChangesNothingWithoutACompletion(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() passClaim(t, store, "a.example", hostclaim.Op{}) passClaim(t, store, "b.example", hostclaim.Op{}) completeOn(t, store, "b.example", "rel-b", "spa", false) passClaim(t, store, "c.example", hostclaim.Op{}) // no row here for _, host := range []string{"a.example", "b.example"} { if err := reg.Upsert(registry.Route{Host: host, Status: "ready", Socket: "/run/" + host + ".sock"}); err != nil { t.Fatal(err) } } before := map[string]string{} for _, host := range []string{"a.example", "b.example", "c.example"} { obs, err := hostclaim.Read(ctx, store, host) if err != nil { t.Fatal(err) } before[host] = obs.ETag } rowsBefore, err := reg.List() if err != nil { t.Fatal(err) } data := t.TempDir() local := &productionClaimLocal{legacyParticipation: true, assembly: &ClaimAssembly{Claims: store, Guard: hostguard.Guard{DataDir: data}, stopLocal: func(context.Context) (claimStopLocal, error) { return claimStopLocal{Actor: hostbox.Actor{Box: "dst"}}, nil }}} claimOnly := func(k string) string { if k == "CARLOS_HOST_CLAIM" { return "1" } return "" } svc := &activator.Service{} r, err := NewRelocationRuntime(ctx, claimOnly, store, local, ClaimProviders{}, reg, svc, relocationRuntimeInputs{}) if err != nil || r == nil { t.Fatal(err) } r.once(ctx, store) for host, etag := range before { if obs, err := hostclaim.Read(ctx, store, host); err != nil || obs.ETag != etag { t.Fatalf("%s was written: %v", host, err) } } if rows, err := reg.List(); err != nil || !reflect.DeepEqual(rows, rowsBefore) { t.Fatalf("the registry changed: %v", err) } if _, err := os.Stat(filepath.Join(data, "relocate")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the pass made journals: %v", err) } if svc.Fenced("a.example") || svc.Fenced("b.example") { t.Fatal("the pass fenced a host") } } // One pass steps every relocating claim (participant, then worker), hands // every landing to the J3 conversion, leaves serving claims alone, and // sweeps the journals of operations that are over (ruling 61). func TestRelocatePassStepsEveryRelocationAndSweeps(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) passClaim(t, store, "a.example", relocatingOp("rel-a")) passClaim(t, store, "b.example", relocatingOp("rel-b")) passClaim(t, store, "c.example", hostclaim.Op{}) enrollment := map[string]string{"src": strings.Repeat("b", 32), "dst": strings.Repeat("c", 32)} landingAtProbed(t, store, "d.example", "mv_abcdefghijklmnopqrstuvwxyz", time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC), enrollment) data := t.TempDir() stale := filepath.Join(data, "relocate", "ops", "rel-gone", "install.json") if err := os.MkdirAll(filepath.Dir(stale), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(stale, []byte(`{"v":1,"host":"c.example","op":"rel-gone"}`), 0o600); err != nil { t.Fatal(err) } var order, converted []string r := &RelocationRuntime{Worker: stepRecorder{name: "worker", order: &order}, Participant: stepRecorder{name: "participant", order: &order}, Box: "dst", DataDir: data, Convert: func(_ context.Context, _ release.Store, o hostclaim.Observed) error { converted = append(converted, o.Claim.Host) return nil }} r.once(ctx, store) want := []string{"participant:a.example", "worker:a.example", "participant:b.example", "worker:b.example"} if !slices.Equal(order, want) { t.Fatalf("stepped %v, want %v", order, want) } if !slices.Equal(converted, []string{"d.example"}) { t.Fatalf("offered for conversion %v, want only the landing d.example", converted) } if _, err := os.Stat(stale); !errors.Is(err, os.ErrNotExist) { t.Fatalf("an ended op's journal survived the pass: %v", err) } } // Rulings 35 and 36: a refusal that never clears (a conversion waiting for // a target) is logged once per host and cause, never per pass. func TestRelocatePassAlertsOnceWhileAConversionWaitsForATarget(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) passClaim(t, store, "a.example", relocatingOp("rel-a")) buf := captureRacyLog(t) var order []string none := errors.New("no eligible box for the converted landing") r := &RelocationRuntime{Worker: stepRecorder{name: "worker", order: &order, err: none}, Participant: stepRecorder{name: "participant", order: &order, err: hostrelocate.ErrNotYet}, Box: "dst"} for range 3 { r.once(ctx, store) } if n := strings.Count(buf.String(), none.Error()); n != 1 { t.Fatalf("logged %d times, want once:\n%s", n, buf.String()) } if strings.Contains(buf.String(), hostrelocate.ErrNotYet.Error()) { t.Fatalf("ErrNotYet was alerted:\n%s", buf.String()) } } // Ruling 49: one pass completes a passed probe on the target: the // participant (its probe stop), then the worker (the serving CAS), then // Finish for the row this box still holds relocating. func TestRelocatePassStopsCompletesAndReleasesInOnePass(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) passClaim(t, store, "a.example", relocatingOp("rel-a")) // Ruling 80: completions are found from the claim's record. One this // box finished already, and one another box finishes, are left alone. passClaim(t, store, "b.example", hostclaim.Op{}) completeOn(t, store, "b.example", "rel-b", "dst", true) passClaim(t, store, "c.example", hostclaim.Op{}) completeOn(t, store, "c.example", "rel-c", "spa", false) var order []string r := &RelocationRuntime{Worker: completingWorker{order: &order, store: store}, Participant: stepRecorder{name: "participant", order: &order}, Box: "dst", Finish: func(_ context.Context, host, op string) error { order = append(order, "finish:"+host+":"+op) return nil }} r.once(ctx, store) if want := []string{"participant:a.example", "worker:a.example", "finish:a.example:rel-a"}; !slices.Equal(order, want) { t.Fatalf("order %v, want %v", order, want) } } // completeOn writes host's serving op with op's completed relocation on // box, marked discharged or not. func completeOn(t *testing.T, store release.Store, host, op, box string, discharged bool) hostclaim.Observed { t.Helper() ctx := t.Context() obs, err := hostclaim.Read(ctx, store, host) if err != nil { t.Fatal(err) } done, _ := json.Marshal(hostclaim.CompletedRelocation{Op: op, Stop: hostclaim.ProbeStop{Box: box, Session: obs.Claim.Holder.Session, Receipt: "fence-1", ReceiptSHA256: strings.Repeat("a", 64)}, Phase: hostclaim.PhaseRelocatingProbing, Discharged: discharged}) obs, err = hostclaim.Update(ctx, store, obs, func(c *hostclaim.Claim) error { c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: box, Participants: []string{box}, Journal: map[string]string{hostclaim.JournalCompleted: string(done)}} return nil }) if err != nil { t.Fatal(err) } return obs } // completingWorker is the worker as the pass sees it when its Step makes // the serving CAS: the claim then serves on dst with the completion. type completingWorker struct { order *[]string store release.Store } func (w completingWorker) Step(ctx context.Context, host string) (hostclaim.Observed, error) { *w.order = append(*w.order, "worker:"+host) obs, err := hostclaim.Read(ctx, w.store, host) if err != nil { return obs, err } done, _ := json.Marshal(hostclaim.CompletedRelocation{Op: obs.Claim.Op.ID, Stop: hostclaim.ProbeStop{Box: "dst", Session: obs.Claim.Holder.Session, Receipt: "fence-1", ReceiptSHA256: strings.Repeat("a", 64)}, Phase: hostclaim.PhaseRelocatingProbing}) return hostclaim.Update(ctx, w.store, obs, func(c *hostclaim.Claim) error { c.Op = hostclaim.Op{Kind: hostclaim.KindServing, Exec: "dst", Participants: []string{"dst"}, Journal: map[string]string{hostclaim.JournalCompleted: string(done)}} return nil }) } // J3: the guard admits a conversion only for the move the claim's op // names, committed for this incarnation with no rollback; anything else is // moves' case (ErrWrongState). func TestCommittedLandingRecordsAdmitOnlyACommittedMove(t *testing.T) { ctx := t.Context() inc, err := hostclaim.Incarnation("bab", "jam", "a.example", time.Date(2026, 10, 7, 1, 0, 0, 0, time.UTC)) if err != nil { t.Fatal(err) } committedAt := time.Date(2026, 10, 7, 5, 0, 0, 0, time.UTC) flipped := committedAt.Add(-time.Hour) o := hostclaim.Observed{Host: "a.example", Claim: &hostclaim.Claim{Host: "a.example", Holder: &hostclaim.Holder{Account: "bab", App: "jam", Incarnation: inc, Location: "instances/a.example"}, Op: hostclaim.Op{Kind: hostclaim.KindLanding, ID: "mv-1", Phase: "probed", Journal: map[string]string{hostclaim.JournalSeal: moves.Seal{V: moves.SealVersion, N: 42, FromDB: true, SrcCovered: true, SealedAt: flipped.Add(-time.Minute), LeaseUntil: flipped, Replica: []moves.SealedObject{{Key: "moves/a.example/mv-1/replica/0009/" + ltx.FormatFilename(1, 42), VersionID: "v1", Size: 100}}}.Encode()}}}} record := func(change func(*provision.MoveState)) MovesRecordReader { return func(context.Context, string, string, string) (*provision.Record, error) { m := &provision.MoveState{ID: "mv-1", Incarnation: inc, From: "uk", To: "ie", State: provision.MoveCommitted, CommittedAt: &committedAt, FlippedAt: &flipped} if change != nil { change(m) } return &provision.Record{Host: "a.example", Account: "bab", App: "jam", Move: m}, nil } } for name, change := range map[string]func(*provision.MoveState){ "not committed": func(m *provision.MoveState) { m.State = provision.MoveLanding }, "another move": func(m *provision.MoveState) { m.ID = "mv-2" }, "another incarnation": func(m *provision.MoveState) { m.Incarnation = "other" }, "rolled back": func(m *provision.MoveState) { m.Rollback = &provision.MoveRollback{} }, "no commit time": func(m *provision.MoveState) { m.CommittedAt = nil }, } { if _, err := (CommittedLandingRecords{Records: record(change)}).CheckLandingCommitted(ctx, o, "mv-1"); !errors.Is(err, hostclaim.ErrWrongState) { t.Fatalf("%s: %v", name, err) } } got, err := (CommittedLandingRecords{Records: record(nil)}).CheckLandingCommitted(ctx, o, "mv-1") if err != nil { t.Fatalf("a committed move: %v", err) } source, staging, err := moves.CommitScopes(o.Claim, &flipped) if err != nil || got.FromRegion != "uk" || got.ToRegion != "ie" || !reflect.DeepEqual(got.SourceScope, source) || !reflect.DeepEqual(got.StagingScope, staging) { t.Fatalf("the commit's regions and scopes: %+v (%v)", got, err) } if _, err := (CommittedLandingRecords{Records: record(nil)}).CheckLandingCommitted(ctx, o, "mv-other"); !errors.Is(err, hostclaim.ErrWrongState) { t.Fatalf("another op id: %v", err) } } // Ruling 30: the release a relocation installs is the host's channel // pointer, verified with the release key; an unverifiable pointer refuses. func TestRelocationReleaseIsTheChannelPointer(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } passClaim(t, store, "a.example", hostclaim.Op{}) if err := provision.CreateRecord(ctx, store, provision.Record{Schema: 1, Host: "a.example", Slug: "a", App: "jam", Account: "bab", Region: "ie", Channel: "stable", CreatedAt: time.Date(2026, 10, 7, 1, 0, 0, 0, time.UTC)}); err != nil && !errors.Is(err, release.ErrCASConflict) { t.Fatal(err) } plat := release.AdoptPlatform() for _, v := range []string{"v1.4.0", "v1.5.0"} { m := release.Manifest{App: "jam", Version: v, Kind: "binary", Artifacts: map[string]release.Artifact{plat: {}}} if err := release.Ship(ctx, store, "bab", m, map[string][]byte{plat: []byte("build " + v)}); err != nil { t.Fatal(err) } } if err := release.Promote(ctx, store, priv, "bab", "jam", "stable", "v1.5.0"); err != nil { t.Fatal(err) } got, err := channelRelease(store, pub)(ctx, "a.example") if err != nil || got != "v1.5.0" { t.Fatalf("release %q %v", got, err) } other, _, _ := ed25519.GenerateKey(rand.Reader) if _, err := channelRelease(store, other)(ctx, "a.example"); err == nil { t.Fatal("an unverifiable pointer answered") } if _, err := channelRelease(store, pub)(ctx, "absent.example"); !errors.Is(err, hostrelocate.ErrRefused) { t.Fatalf("a host with no claim: %v", err) } } // Ruling 3: a terminated box's local state is discharged only with the // roster's enrolled_at; without it the discharge is unqualified, with it // boxvolumes.Discharge's verdict. func TestDischargeIsUnqualifiedWithoutEnrolledAt(t *testing.T) { ctx := t.Context() store := release.NewDirStore(t.TempDir()) discharge := volumeDischarge(store) fence := termfence.Record{ObservedAt: time.Date(2026, 10, 7, 9, 0, 0, 0, time.UTC).Format(time.RFC3339Nano)} box := hostactors.Box{BoxID: "src", Enrollment: strings.Repeat("b", 32), Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-src"}} if d := discharge(ctx, box, fence); d == nil || d.Qualified || !strings.Contains(d.Reason, "enrollment time") { t.Fatalf("without enrolled_at: %+v", d) } box.EnrolledAt = time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) records, err := boxvolumes.List(ctx, store, box.Enrollment) if err != nil { t.Fatal(err) } at, _ := time.Parse(time.RFC3339Nano, fence.ObservedAt) want := boxvolumes.Discharge(records, boxvolumes.Entry{Box: box.BoxID, Enrollment: box.Enrollment, Cloud: box.Cloud}, box.EnrolledAt, at) if d := discharge(ctx, box, fence); d == nil || d.Qualified != want.Discharge || d.Reason != want.Reason { t.Fatalf("with enrolled_at: %+v, want %+v", d, want) } if d := discharge(ctx, box, termfence.Record{ObservedAt: "not a time"}); d == nil || d.Qualified { t.Fatalf("an unreadable observation: %+v", d) } } // Rulings 50 and 51: the data bucket is opened as a release.Store of its // own (production's lease.VersionedStore is never asserted to one), the // versioned client and the leases sign for the roster's cloud region, and // genselect, pins and the worker use the logical region. func TestRelocationRuntimeOpensADataBucketStore(t *testing.T) { ctx := t.Context() files := release.NewDirStore(t.TempDir()) var opened, signed string self := hostactors.Box{Region: "ie", Cloud: hostactors.Cloud{Region: "eu-west-1"}} s, err := openRelocationStores(ctx, self, "data-ie", func(_ context.Context, bucket string) (release.Store, error) { opened = bucket; return files, nil }, func(_ context.Context, _ string, region string) (lease.VersionedStore, error) { signed = region return leasetest.NewVersionedBucket(), nil }) if err != nil { t.Fatal(err) } if opened != "data-ie" || signed != "eu-west-1" || s.Files != files || s.LeaseRegion != "eu-west-1" || s.Region != "ie" || s.Bucket != "data-ie" { t.Fatalf("stores %+v (opened %q, signed %q)", s, opened, signed) } guard := ClaimAssembly{} guard.Guard.DataDir = "/data" src := hostbox.WorkSource{Box: "dst", Region: "ie"} cfg, err := relocationAssemblyConfig(&guard, ClaimProviders{Selector: src}, s, ®istry.Registry{}, &relocationProbeActivator{}, hostactors.Directory{}, "/opt/carlos", func(context.Context, hostclaim.Observed) error { return nil }) if err != nil { t.Fatal(err) } if cfg.Data != files || cfg.Versions != s.Versions || cfg.LeaseRegion != "eu-west-1" || cfg.Region != "ie" || cfg.WorkDir != "/data/relocate/build" || cfg.ReleaseDir != "/data/relocate/release" || cfg.OptDir != "/opt/carlos" || cfg.Source.Box != "dst" { t.Fatalf("config %+v", cfg) } if _, err := openRelocationStores(ctx, hostactors.Box{Region: "ie"}, "data-ie", func(context.Context, string) (release.Store, error) { return files, nil }, func(context.Context, string, string) (lease.VersionedStore, error) { return nil, nil }); err == nil { t.Fatal("opened with no cloud region to sign for") } } // Finding 11, N5, ruling 53: region HA's owners bind only with moves' C8 // provider. The production gate hands the registered source main's own // serving sources before it opens; with C8 bound the owners are complete // and relocating binds; without it, or with region HA off, nothing binds. func TestRelocationBindsWithARegisteredMovesSource(t *testing.T) { ctx := t.Context() actor := hostbox.Actor{Box: "dst", Bucket: "data-ie", Region: "ie"} serving := func(files ClaimFilesManifest) ClaimOwnerBindings { var out ClaimOwnerBindings out.Serving = ClaimServingSources{Launch: func(context.Context, registry.Route) (ClaimServingLaunch, error) { return ClaimServingLaunch{}, nil }, Markers: plan9D5Markers{leasetest.NewVersionedBucket()}, Files: files} return out } source := func() *RelocationSource { return &RelocationSource{DataDir: t.TempDir(), FilesStore: func(context.Context, string) (release.Store, error) { return release.NewDirStore(t.TempDir()), nil }} } out := serving(&roFiles{}) registered := source() closeSource, err := openClaimRelocationOwners(ctx, plan11Env("1", "1"), registered, release.NewDirStore(t.TempDir()), &fakeVersioned{}, actor, &out) if err != nil || closeSource == nil { t.Fatalf("open: %v", err) } // Carry 5: the source's identity check is the owners' own // IdentityProvider.Verify, as main's serving owners bind it. This // one's provider is unconfigured, so it refuses as Verify does. registered.mu.Lock() identity := registered.serving.Identity registered.mu.Unlock() if identity == nil { t.Fatal("the source was handed no identity check") } if _, want := out.Identity.Verify(ctx); want == nil || !errors.Is(identity(ctx), want) { t.Fatalf("the identity check is not the owners' Verify: %v", identity(ctx)) } if bound, err := claimRelocationC1(plan11Env("1", "1"), true, out.Relocation); !bound || err != nil || len(out.PolicyRefusals) != 2 { t.Fatalf("with C8: bound %v %v refusals %d", bound, err, len(out.PolicyRefusals)) } without := serving(nil) if _, err := openClaimRelocationOwners(ctx, plan11Env("1", "1"), source(), release.NewDirStore(t.TempDir()), &fakeVersioned{}, actor, &without); err != nil || !without.Relocation.zero() { t.Fatalf("without C8: %+v %v", without.Relocation, err) } off := serving(&roFiles{}) if closeSource, err := openClaimRelocationOwners(ctx, plan11Env("1", ""), source(), release.NewDirStore(t.TempDir()), &fakeVersioned{}, actor, &off); err != nil || closeSource != nil || !off.Relocation.zero() { t.Fatalf("region HA off: %+v %v", off.Relocation, err) } // Without the bound owners there is no relocation pass, only the // completion-only runtime (ruling 82). reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db")) if err != nil { t.Fatal(err) } defer reg.Close() local := &productionClaimLocal{legacyParticipation: true, assembly: &ClaimAssembly{Wake: &ClaimWakeAssembly{Inputs: ClaimOwnerDispatch{}}, Guard: hostguard.Guard{DataDir: t.TempDir()}}} r, err := NewRelocationRuntime(ctx, plan11Env("1", "1"), release.NewDirStore(t.TempDir()), local, ClaimProviders{}, reg, &activator.Service{}, relocationRuntimeInputs{}) if err != nil || r == nil || r.Worker != nil || r.Participant != nil || r.Finish == nil { t.Fatalf("a runtime without the relocation owners: %+v %v", r, err) } }