//go:build linux package host import ( "context" "fmt" "testing" "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/registry" "github.com/carlosframework/platform/internal/rootstate" "github.com/carlosframework/platform/internal/schedule" ) // witnessPolicy is the production claim policy as claimconstruction.go // composes it (hostbox.Policy with the registry hold policy, then // WithRefusals) carrying claimproduction.go's unit-shape refusal from // productionClaimUnitShapePolicy. The witnesses used an allow-all policy, // which hid that the refusal refused every fresh install (harness G10, // 2026-10-21). // // The refusal runs in instance-control mode: agent mode asks root's // boundInputs over the role-launch socket, which these in-process worlds do // not serve. Both modes share the row lookup this pins, and // TestClaimUnitShapePolicyFreshInstallAgentReader covers root's reader. The // unit-binding store and claim-units configuration are empty seams: no // configured shape and no saved binding, as on a box with no bound units. // missing counts the lookups that found no row. func witnessPolicy(t *testing.T, reg *registry.Registry) (policy func(context.Context, *hostclaim.Claim, hostbox.Selection, hostwrite.Identity) error, missing *int) { t.Helper() oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes t.Cleanup(func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }) openClaimUnitBindingsRead = func() (rootstate.Store, error) { return &rollbackMemory{files: map[string][]byte{}}, nil } productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{}, nil } missing = new(int) route := func(host string) (*registry.Route, error) { rt, err := reg.Get(host) if err == nil && rt == nil { *missing++ } return rt, err } return hostbox.Policy{Held: hostbox.RegistryHoldPolicy(reg)}.WithRefusals(productionClaimUnitShapePolicy("instance-control", route)).Check, missing } // G10 (harness, 2026-10-21): the first claim install on an empty box, through // the remote path into root's real commitRootInventory, under the production // policy. The install's first work.With (inventory_registration.go's route // entry) runs before the registry row exists; the unit-shape refusal used to // answer ErrChanged there on every attempt, leaving root's inventory at // revision 1 with no route entry and no row. func TestClaimFreshInstallUnderProductionPolicy(t *testing.T) { w := newBindingInstallWorld(t) policy, missing := witnessPolicy(t, w.reg) w.source.Policy = policy w.adapter.Reconciler.Writes = hostwrite.Config{Enabled: true, Selector: w.source} if row, err := w.reg.Get(w.rec.Host); err != nil || row != nil { t.Fatalf("not a fresh install: row %v, %v", row, err) } w.install(t) if *missing == 0 { t.Fatal("the unit-shape refusal never saw the fresh install's missing row") } in, revision := w.inventory(t) if revision != 3 || len(in.Entries) != 1 || in.Entries[0].Kind != "route" || in.Entries[0].Materialization != "materialized" || in.DirectoryRevision != w.pinned.Binding { t.Fatalf("inventory revision %d, entries %+v, stamp %q", revision, in.Entries, in.DirectoryRevision) } row, err := w.reg.Get(w.rec.Host) if err != nil || row == nil || row.ClaimOwnership != w.route.ClaimOwnership || row.ClaimRowID != in.Entries[0].RowIdentity { t.Fatalf("registry row %+v, %v", row, err) } holder := w.f.read().Claim.Holder if row.ClaimIncarnation != holder.Incarnation || row.ClaimStamp != holder.Stamp { t.Fatalf("row owned by %s/%d, holder %s/%d", row.ClaimIncarnation, row.ClaimStamp, holder.Incarnation, holder.Stamp) } view, err := w.rootAuth.View(t.Context(), w.rec.Host, hostguard.Captured{Incarnation: holder.Incarnation, Stamp: holder.Stamp}) if err != nil || view.Installation.InventoryRevision != revision { t.Fatalf("guard inventory revision %d (journal %d), %v", view.Installation.InventoryRevision, revision, err) } } // Fix round 2 (Astra on 4dd4c436): only the installation's own work may go // on without a registry row. A timer the runner selected for a serving host // keeps its cached route; when the route is removed after that read, while // the claim, installation, timer row and running producer all survive, the // production policy must refuse both the delivery's producer registration // (ClaimTimerDelivery) and the runner's bookkeeping (Runner.record's // MarkTimerFired, which runs inside the timer work's With exactly as here). func TestClaimTimerRefusedAfterRouteRemoval(t *testing.T) { w := newBindingInstallWorld(t) w.install(t) ctx := t.Context() host := w.rec.Host if err := w.reg.UpsertTimer(registry.Timer{Host: host, Name: "nightly", Kind: schedule.KindOnce, Spec: "once", Path: "/tick", NextAt: 1}); err != nil { t.Fatal(err) } timer, err := w.reg.GetTimer(host, "nightly") if err != nil || timer == nil { t.Fatal(timer, err) } route, err := w.reg.Get(host) // the runner's cached route if err != nil || route == nil || !route.ClaimOwnership.Valid() { t.Fatal(route, err) } timer.ClaimOwnership = route.ClaimOwnership // as a claim box's timer rows carry it holder := w.f.read().Claim.Holder work, err := w.source.Select(ctx, host, "timer") if err != nil { t.Fatal(err) } if s, err := hostbox.Selected(work); err != nil || s.Installation() { t.Fatal("timer work is marked as installation work", err) } target := hostguard.FenceTarget{Host: host, Incarnation: holder.Incarnation, Stamp: holder.Stamp, Session: holder.Session, Box: w.pinned.Box, Enrollment: w.pinned.Enrollment, ControllerID: "controller"} var registered []string p, err := NewClaimProducers(target, "start", func(_ context.Context, _ hostguard.FenceTarget, id string) error { registered = append(registered, id) return nil }) if err != nil { t.Fatal(err) } set := &ClaimProducerSet{} if err := set.Add(p); err != nil { t.Fatal(err) } seq := 0 deliver := ClaimTimerDelivery(set, func() string { seq++; return fmt.Sprintf("d%d", seq) }) mark := func(at int64) (bool, error) { var applied bool err := work.With(ctx, hostwrite.Identity{}, func() error { var e error applied, e = w.reg.MarkTimerFired(host, timer.Name, timer.Registration, at, "200", 5, 0, 0, 0) return e }) return applied, err } // With the route present the same work delivers and records. _, finish, err := deliver(ctx, work, *route, *timer) if err != nil || len(registered) != 1 { t.Fatalf("delivery with the route present: %v, registrations %v", err, registered) } finish() if applied, err := mark(100); err != nil || !applied { t.Fatalf("bookkeeping with the route present: applied %v, %v", applied, err) } before, err := w.reg.GetTimer(host, "nightly") if err != nil || before == nil || before.LastAt != 100 { t.Fatal(before, err) } // The route goes after the runner read it. if err := w.reg.Delete(host); err != nil { t.Fatal(err) } if rt, err := w.reg.Get(host); err != nil || rt != nil { t.Fatal("route still present", rt, err) } if still, err := w.reg.GetTimer(host, "nightly"); err != nil || still == nil { t.Fatal("the timer row went with the route; this witness needs it to survive", err) } if _, _, err := deliver(ctx, work, *route, *timer); err == nil || len(registered) != 1 { t.Errorf("delivered after the route was removed: %v, registrations %v", err, registered) } if applied, err := mark(200); err == nil || applied { t.Errorf("bookkeeping after the route was removed: applied %v, %v", applied, err) } after, err := w.reg.GetTimer(host, "nightly") if err != nil || after == nil || *after != *before { t.Errorf("timer row changed after the route was removed: %+v (was %+v), %v", after, before, err) } }