package host import ( "context" "crypto/ed25519" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/hostactors" "github.com/carlosframework/platform/internal/hostbox" "github.com/carlosframework/platform/internal/hostclaim" execlease "github.com/carlosframework/platform/internal/hostexec" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/hostguard" "github.com/carlosframework/platform/internal/hostwrite" "github.com/carlosframework/platform/internal/release" ) func TestPlan9ProductionRosterRequiresPublishedMatchingFloor(t *testing.T) { key := ed25519.NewKeyFromSeed(make([]byte, 32)) s := release.NewDirStore(t.TempDir()) bucket := "deployment" floor := hostfloor.Floor{Min: 1, MinRelease: "v0.1.0"} if err := hostfloor.Publish(t.Context(), s, release.RootSigner(key), bucket, floor); err != nil { t.Fatal(err) } raw, _, _ := s.Get(t.Context(), hostfloor.Key) auth, _, _ := s.Get(t.Context(), hostfloor.AuthKey) dir := t.TempDir() policy := hostfloor.Policy{Schema: 1, DeploymentBucket: bucket, Mode: "enforced", Floor: raw, Authorization: auth} body, _ := json.Marshal(policy) if err := os.WriteFile(filepath.Join(dir, "policy.json"), body, 0600); err != nil { t.Fatal(err) } row := hostactors.Box{BoxID: "box", Enrollment: strings.Repeat("a", 32), Namespace: "platform", Label: "box", Region: "ie", Pool: "default", Cloud: hostactors.Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "i-box"}, EvidenceSHA256: strings.Repeat("c", 64), State: "active"} roster := hostactors.Roster{V: 1, DeploymentBucket: bucket, Revision: 1, RequestLimitNS: int64(hostbox.RequestLimit), Publication: "published", FloorSHA256: hostfloor.Digest(raw), Boxes: map[string]hostactors.Box{"box": row}, Consoles: map[string]hostactors.Console{}, Holds: map[string]hostactors.Reservation{}, Activations: map[string]hostactors.Reservation{}} write := func() { wire, err := release.SignRootEnvelope(release.RootSigner(key), roster) if err != nil { t.Fatal(err) } _, etag, _ := s.Get(t.Context(), hostactors.Key) if _, err = s.Put(t.Context(), hostactors.Key, wire, etag); err != nil { t.Fatal(err) } } write() provider := productionClaimRoster{Directory: hostactors.Directory{Store: s, PublicKey: key.Public().(ed25519.PublicKey), Bucket: bucket}, DataBucket: "data", FloorDir: dir} actor, err := provider.Actor(t.Context(), "box") if err != nil { t.Fatal(err) } if actor.Bucket != "data" || actor.ControlBucket != bucket { t.Fatal("mixed bucket authority") } roster.Publication = "preparing" write() if _, err = provider.Actor(t.Context(), "box"); err == nil { t.Fatal("accepted unpublished floor") } roster.Publication = "published" row.State = "quarantined" row.QuarantineRevision = 1 row.LossIDs = []string{"loss"} roster.Boxes["box"] = row write() if _, err = provider.Actor(t.Context(), "box"); err == nil { t.Fatal("accepted quarantined actor") } } func TestPlan9ProductionLocalServingDoesNotCreateLease(t *testing.T) { dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, ".hostexec"), 0700); err != nil { t.Fatal(err) } p := productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: dir}, slots: map[string]*claimParticipation{}} c := &hostclaim.Claim{Host: "x.example", Holder: &hostclaim.Holder{Incarnation: "inc"}, Op: hostclaim.Op{Kind: hostclaim.KindServing}} actor := hostbox.Actor{Box: "box", Region: "ie"} selection := hostbox.Selection{Box: "box", Region: "ie", Captured: hostguard.Captured{Incarnation: "inc"}} if err := p.Check(t.Context(), actor, c, selection); err != nil { t.Fatal(err) } entries, err := os.ReadDir(filepath.Join(dir, ".hostexec")) if err != nil || len(entries) != 0 { t.Fatalf("serving created lease: %v %v", entries, err) } c.Op.Kind = hostclaim.KindLanding c.Op.ID = "operation" if err := p.Check(t.Context(), actor, c, selection); err == nil { t.Fatal("active operation used no controller") } p.closed = true if err := p.Check(t.Context(), actor, c, selection); !errors.Is(err, execlease.ErrLost) { t.Fatalf("closed=%v", err) } } func TestPlan9ProductionMissingMovesSourcesAreTypedRefusals(t *testing.T) { p := productionClaimRegional{} if _, err := p.View(context.Background(), hostclaim.Observed{}); err == nil { t.Fatal("missing view granted") } if _, err := p.LeaseSet(context.Background(), "bucket", "scope"); err == nil { t.Fatal("missing staging lease inventory granted") } if _, _, err := p.VerifyRetirement(context.Background(), hostclaim.Observed{}, nil); err == nil { t.Fatal("missing archived evidence granted") } } func TestPlan9ParticipationCleanupDoesNotWaitForItsOpeningSlot(t *testing.T) { f := newRecoveryFixture(t) controller := f.controller("boot-1", "controller-1", 4101, "start-1") o := controller.admit(t, f) controller.assembly.Reconcile = &hostbox.ReconcileAdapter{Census: &hostbox.EmptyInstallationCensus{}} pending := &claimParticipation{Ready: make(chan struct{})} p := &productionClaimLocal{legacyParticipation: true, assembly: &controller.assembly, slots: map[string]*claimParticipation{recoveryHost: pending}} ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() if err := p.cleanup(ctx, o, execlease.Binding{}); err != nil { t.Fatal(err) } select { case <-pending.Ready: t.Fatal("cleanup altered the opening slot") default: } if len(f.receipts()) != 1 { t.Fatal("cleanup omitted root stop evidence") } } func TestPlan9ParticipationFailedOpenCanDischargeAfterPositiveCleanup(t *testing.T) { t.Parallel() f := newRecoveryFixture(t) o := f.read() if err := os.Mkdir(filepath.Join(f.guard.DataDir, ".hostexec"), 0700); err != nil { t.Fatal(err) } identity := handoverRecoveryIdentity(t, hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 1, Binding: "1", Region: "ie"}) ready := make(chan struct{}) close(ready) slot := &claimParticipation{Ready: ready, Err: execlease.ErrLost, CleanupSucceeded: true, Captured: o, Config: execlease.Config{Binding: execlease.Binding{Host: recoveryHost, Box: "box-1", Enrollment: "enrollment-1", Op: "old-op", Incarnation: o.Claim.Holder.Incarnation}, Guard: f.guard, LockDir: t.TempDir()}} p := &productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: f.guard.DataDir}, Identity: identity, assembly: &ClaimAssembly{}, lifetime: t.Context(), slots: map[string]*claimParticipation{recoveryHost: slot}} if err := p.Prepare(t.Context(), o); err != nil { t.Fatal(err) } if p.slots[recoveryHost] != nil { t.Fatal("historical Open error prevented CC9 discharge") } } func TestPlan9ParticipationCleanupSurvivesRosterWithdrawal(t *testing.T) { f := newRecoveryFixture(t) controller := f.controller("boot-1", "controller-1", 4101, "start-1") o := controller.admit(t, f) controller.assembly.Reconcile = &hostbox.ReconcileAdapter{Census: &hostbox.EmptyInstallationCensus{}} p := &productionClaimLocal{legacyParticipation: true, slots: map[string]*claimParticipation{}} if err := p.bind(t.Context(), &controller.assembly); err != nil { t.Fatal(err) } // New authority fails after withdrawal. Cleanup retains only this already // verified process identity; root still checks the exact captured claim. controller.assembly.stopLocal = nil controller.assembly.Identity = hostbox.IdentityProvider{} ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() if err := p.cleanup(ctx, o, execlease.Binding{}); err != nil { t.Fatalf("withdrawal prevented cleanup: %v", err) } if len(f.receipts()) != 1 { t.Fatal("cleanup omitted root evidence") } } func TestPlan9RootAndAdoptLeaveStaleParticipationForAgent(t *testing.T) { for _, mode := range []string{"adopt", "instance-control"} { t.Run(mode, func(t *testing.T) { f := newRecoveryFixture(t) o := f.read() directory := filepath.Join(f.guard.DataDir, ".hostexec") if err := os.Mkdir(directory, 0700); err != nil { t.Fatal(err) } state := execlease.State{V: 1, Binding: execlease.Binding{Host: o.Host, Box: "box-1", Enrollment: "enrollment-1", Op: "old-op", Incarnation: o.Claim.Holder.Incarnation, BootID: "boot", ControllerID: "old"}, State: "lost", SafeUntil: 1, Expires: time.Now()} raw, err := json.Marshal(state) if err != nil { t.Fatal(err) } path := filepath.Join(directory, o.Host+".json") if err = os.WriteFile(path, raw, 0600); err != nil { t.Fatal(err) } actor := hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 1, Binding: "1", Region: "ie"} // No cleanup dependencies: a root observer must refuse before touching // the journal, root socket or lifetime locks. p := productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: mode, DataDir: f.guard.DataDir}, assembly: &ClaimAssembly{}, lifetime: t.Context(), slots: map[string]*claimParticipation{}} if err = p.prepareWith(t.Context(), actor, o); err == nil { t.Fatal("root admitted without root participation") } after, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(raw) != string(after) { t.Fatal("root rewrote stale participation") } }) } } func TestPlan9ParticipationDoesNotSkipPreviousAgentState(t *testing.T) { t.Parallel() for _, serving := range []bool{true, false} { f := newRecoveryFixture(t) o := f.read() if !serving { o.Claim.Op = hostclaim.Op{Kind: hostclaim.KindDeleting, ID: "elsewhere", Phase: "stop", Exec: "box-2", Participants: []string{"box-2"}} body, _ := json.Marshal(o.Claim) key, _ := hostclaim.Key(o.Host) if _, err := f.claims.Put(t.Context(), key, body, o.ETag); err != nil { t.Fatal(err) } } directory := filepath.Join(f.guard.DataDir, ".hostexec") if err := os.Mkdir(directory, 0700); err != nil { t.Fatal(err) } state := execlease.State{V: 1, Binding: execlease.Binding{Host: o.Host, Box: "box-1", Enrollment: "enrollment-1", Op: "previous", Incarnation: o.Claim.Holder.Incarnation, BootID: "boot", ControllerID: "previous"}, State: "lost", SafeUntil: 1, Expires: time.Now()} body, _ := json.Marshal(state) if err := os.WriteFile(filepath.Join(directory, o.Host+".json"), body, 0600); err != nil { t.Fatal(err) } actor := hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 1, Binding: "1", Region: "ie"} p := productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: f.guard.DataDir}, Identity: handoverRecoveryIdentity(t, actor), Store: f.claims, assembly: &ClaimAssembly{}, lifetime: t.Context(), slots: map[string]*claimParticipation{}} // Missing cleanup proof must surface. Success would mean the scan silently // skipped a surviving prior controller just because no slot exists in RAM. if err := p.participate(t.Context()); !errors.Is(err, hostwrite.ErrUnavailable) { t.Fatalf("serving=%v skipped previous controller: %v", serving, err) } } } type plan9DeadlineStore struct { release.Store deadline time.Time } func (s *plan9DeadlineStore) Get(ctx context.Context, key string) ([]byte, string, error) { if key == "control/hosts/"+recoveryHost+".json" { s.deadline, _ = ctx.Deadline() } return s.Store.Get(ctx, key) } func TestPlan9ParticipationBoundsEachClaimReadIndependently(t *testing.T) { t.Parallel() f := newRecoveryFixture(t) actor := hostbox.Actor{Box: "box-1", Enrollment: "enrollment-1", Revision: 1, Binding: "1", Region: "ie"} store := &plan9DeadlineStore{Store: f.claims} p := productionClaimLocal{legacyParticipation: true, Resources: ClaimRuntimeResources{Mode: "agent", DataDir: f.guard.DataDir}, Identity: handoverRecoveryIdentity(t, actor), Store: store, slots: map[string]*claimParticipation{}} before := time.Now() if err := p.participate(t.Context()); err != nil { t.Fatal(err) } if store.deadline.IsZero() || store.deadline.Before(before.Add(time.Minute)) || store.deadline.After(time.Now().Add(hostbox.RequestLimit)) { t.Fatalf("claim read lacks independent request budget: %v", store.deadline) } }