package host import ( "context" "errors" "os" "strings" "testing" "github.com/carlosframework/platform/internal/caps" "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" ) func TestPlan14UnitShapeAllOperationKinds(t *testing.T) { t.Parallel() for _, unit := range []string{"console.service", "woodstar-socket.service", "carlos-app@shop.example.service"} { t.Run(unit, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, unit, "", "") if strings.HasPrefix(unit, "carlos-app@") { shape.Backing = "unit" } if unit == "woodstar-socket.service" { shape.Addr = "" shape.Socket = "/run/carlos/woodstar.sock" } b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } candidate := shape.route() read := func(ctx context.Context, rt registry.Route, s hostbox.Selection) error { return checkClaimUnitShape(ctx, b, map[string]claimUnitShape{in.Host: shape}, rt, s) } refuse := claimUnitShapePolicy(func(string) (*registry.Route, error) { return &candidate, nil }, read) for _, kind := range []string{hostclaim.KindServing, hostclaim.KindMoving, hostclaim.KindLanding, hostclaim.KindReverting, hostclaim.KindRestoring, hostclaim.KindRelocating} { for _, action := range []string{"install", "restore", "spawn", "writer", "config", "release", "timers", "held-config", "held-release", "held-timers", "probe"} { s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}, Action: action, Kind: kind} c := &hostclaim.Claim{Host: in.Host, Op: hostclaim.Op{Kind: kind}} candidate = shape.route() if err := refuse(t.Context(), c, s, hostwrite.Identity{}); err != nil { t.Fatal(kind, action, err) } for _, mutate := range []func(*registry.Route){ func(r *registry.Route) { r.Unit = ""; r.Backing = "" }, func(r *registry.Route) { r.Unit = "sibling.service" }, func(r *registry.Route) { r.Backing = "exec" }, func(r *registry.Route) { r.Account = "other" }, func(r *registry.Route) { r.App = "other" }, func(r *registry.Route) { r.Kind = "sidecar" }, func(r *registry.Route) { r.DBPath = "/data/sibling.db" }, func(r *registry.Route) { r.Socket = "/run/carlos/sibling.sock" }, func(r *registry.Route) { r.Addr = "127.0.0.1:9999" }, func(r *registry.Route) { r.Tunnel = "other" }, func(r *registry.Route) { r.Replica = true }, func(r *registry.Route) { r.Seed = "seed" }, } { candidate = shape.route() mutate(&candidate) if err := refuse(t.Context(), c, s, hostwrite.Identity{}); err == nil { t.Fatal("shape edit reached operation", kind, action, candidate) } } } } s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp + 1}} candidate = shape.route() if err := refuse(t.Context(), nil, s, hostwrite.Identity{}); err == nil { t.Fatal("successor used predecessor binding") } sibling := registry.Route{Host: "sibling.example", Kind: "instance"} if err := checkClaimUnitShape(t.Context(), b, map[string]claimUnitShape{in.Host: shape}, sibling, hostbox.Selection{Host: sibling.Host}); err != nil { t.Fatal("ordinary sibling refused", err) } }) } } func TestPlan14BoundCapabilitiesRefuseEffects(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }() openClaimUnitBindingsRead = func() (rootstate.Store, error) { return b.state, nil } for _, config := range []string{"good", "removed", "corrupt"} { t.Run(config, func(t *testing.T) { productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { if config == "corrupt" { return nil, os.ErrPermission } if config == "removed" { return map[string]claimUnitShape{}, nil } return map[string]claimUnitShape{in.Host: shape}, nil } dir := t.TempDir() ops := productionClaimCapsOps(caps.Ops{EtcDir: dir, Systemctl: func(...string) error { t.Fatal("bound capability reached systemctl"); return nil }}, func() (bool, error) { return false, nil }) if _, err := caps.Converge(ops, caps.Plan{Host: in.Host}); err == nil { t.Fatal("converge accepted bound host") } if err := caps.Restore(ops, caps.Snapshot{Host: in.Host}); err == nil { t.Fatal("restore accepted bound host") } if err := caps.Restart(ops, caps.Plan{Host: in.Host}); err == nil { t.Fatal("restart accepted bound host") } entries, err := os.ReadDir(dir) if err != nil || len(entries) != 0 { t.Fatal("wrote capability dropin", entries, err) } if err := claimBoundCapsRefusal("ordinary.example"); err != nil { t.Fatal("ordinary host refused", err) } }) } } func TestPlan14BoundCapabilitiesNoCensus(t *testing.T) { oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }() openClaimUnitBindingsRead = func() (rootstate.Store, error) { t.Fatal("no-census binding read"); return nil, os.ErrPermission } productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { t.Fatal("no-census config read") return nil, os.ErrPermission } calls := 0 prior := caps.Ops{CheckHost: func(string) error { calls++; return nil }} if err := productionClaimCapsOps(prior, func() (bool, error) { return true, nil }).CheckHost("console.example"); err != nil || calls != 1 { t.Fatal(err, calls) } if err := productionClaimCapsOps(prior, func() (bool, error) { return false, os.ErrPermission }).CheckHost("console.example"); err == nil || calls != 1 { t.Fatal("census error reached effect", err, calls) } } func TestPlan14UnitShapePolicyProductionModes(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }() opens := 0 openClaimUnitBindingsRead = func() (rootstate.Store, error) { opens++; return b.state, nil } productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{in.Host: shape}, nil } for _, mode := range []string{"instance-control", "adopt"} { row := shape.route() p := productionClaimUnitShapePolicy(mode, func(string) (*registry.Route, error) { return &row, nil }) s := hostbox.Selection{Host: in.Host, Box: in.Box, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}} if err := p(t.Context(), nil, s, hostwrite.Identity{}); err != nil { t.Fatal(mode, err) } row.Unit = "" if err := p(t.Context(), nil, s, hostwrite.Identity{}); err == nil { t.Fatal(mode, "cleared binding") } } if opens != 4 { t.Fatal("root did not read independent binding", opens) } openClaimUnitBindingsRead = func() (rootstate.Store, error) { return nil, os.ErrPermission } p := productionClaimUnitShapePolicy("adopt", func(string) (*registry.Route, error) { r := shape.route(); return &r, nil }) if p(t.Context(), nil, hostbox.Selection{Host: in.Host}, hostwrite.Identity{}) == nil { t.Fatal("unavailable root read permitted") } } func TestPlan14BoundRestartShapeRefusal(t *testing.T) { for _, op := range []string{"restart", "rollback", "checked"} { for _, shape := range []string{"unchanged", "cleared", "ordinary", "sibling", "backing", "socket", "missing"} { t.Run(op+"/"+shape, func(t *testing.T) { reg := restartTestRegistry(t) b, in, bound := plan14UnitBindingFixture(t, "console.service", "", "") if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } row := bound.route() switch shape { case "cleared": row.Unit = "" case "ordinary": row.Unit = "carlos-app@" + in.Host + ".service" case "sibling": row.Unit = "sibling.service" case "backing": row.Backing = "unit" case "socket": row.Socket = "/run/carlos/other.sock" } if shape != "missing" { if err := reg.Upsert(row); err != nil { t.Fatal(err) } } calls := 0 oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }() openClaimUnitBindingsRead = func() (rootstate.Store, error) { return b.state, nil } productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { return map[string]claimUnitShape{in.Host: bound}, nil } checks := rootRestartChecks{unitShape: func(ctx context.Context, host string, rt *registry.Route) error { calls++ return rootClaimRestartShape(ctx, host, rt) }} sys := &fakeUnits{} var err error switch op { case "restart": err = rootRestartRouteWith(reg, sys, t.TempDir(), func(string) string { return "" }, nil, checks)(in.Host) case "rollback": err = rollbackUnitRestart(reg, sys, t.TempDir(), func(string) string { return "" }, checks)(in.Host) case "checked": if shape == "missing" { row = registry.Route{Host: in.Host} } err = checkedUnitRestart(sys, t.TempDir(), func(string) string { return "" }, nil, checks)(row) } if err == nil || calls != 1 || len(sys.restarts) != 0 { t.Fatal("shape bypass", err, calls, sys.restarts) } }) } } } // A fresh install has no registry row until registerRemote's second // work.With writes it, and the unit-shape refusal runs on every work.With // before that (harness G10, 2026-10-21). Only the installation's own work // (hostbox.Selection.Installation) may go on without a row, and then the // refusal checks an empty route for the host: it passes only when the host // has no configured shape and no saved unit binding. Any other work refuses // a missing row (fix round 2). func TestClaimUnitShapePolicyFreshInstall(t *testing.T) { oldOpen, oldShapes := openClaimUnitBindingsRead, productionClaimUnitShapes defer func() { openClaimUnitBindingsRead, productionClaimUnitShapes = oldOpen, oldShapes }() for _, tc := range []struct { name string row string // "", "other", "matching", "ordinary" shape bool binding bool storeless bool want error // for the installation's own work }{ {name: "no row, no shape, no binding", want: nil}, {name: "no row, no binding store", storeless: true, want: nil}, {name: "no row, configured shape", shape: true, want: hostbox.ErrUnsupported}, {name: "no row, saved binding", shape: true, binding: true, want: hostbox.ErrChanged}, {name: "no row, saved binding, shape removed", binding: true, want: hostbox.ErrChanged}, {name: "row for another host", row: "other", want: hostbox.ErrChanged}, {name: "matching row", row: "matching", shape: true, binding: true, want: nil}, {name: "matching ordinary row", row: "ordinary", want: nil}, } { t.Run(tc.name, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") if tc.binding { if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } } openClaimUnitBindingsRead = func() (rootstate.Store, error) { if tc.storeless { return nil, os.ErrNotExist } return b.state, nil } productionClaimUnitShapes = func() (map[string]claimUnitShape, error) { if tc.shape { return map[string]claimUnitShape{in.Host: shape}, nil } return map[string]claimUnitShape{}, nil } var row *registry.Route switch tc.row { case "other": r := registry.Route{Host: "sibling.example", Kind: "instance"} row = &r case "matching": r := shape.route() row = &r case "ordinary": r := registry.Route{Host: in.Host, Account: in.Account, App: in.App, Kind: "instance", DBPath: "/data/instances/shop/data.db", Addr: "127.0.0.1:8090"} row = &r } lookups := 0 route := func(host string) (*registry.Route, error) { lookups++ if host != in.Host { t.Fatal("looked up", host) } return row, nil } s := hostbox.Selection{Host: in.Host, Box: in.Box, Action: "reconcile", Kind: hostclaim.KindServing, Captured: hostguard.Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}} if s.Installation() { t.Fatal("a selection literal is marked as installation work") } check := func(name string, err, want error) { t.Helper() if want == nil && err != nil { t.Fatalf("%s: refused: %v", name, err) } if want != nil && !errors.Is(err, want) { t.Fatalf("%s: got %v, want %v", name, err, want) } } // Ordinary work, through the production policy: the selection is // not an installation's, so a missing row refuses. ordinary := tc.want if row == nil { ordinary = hostbox.ErrChanged } p := productionClaimUnitShapePolicy("instance-control", route) check("ordinary work", p(t.Context(), &hostclaim.Claim{Host: in.Host, Op: hostclaim.Op{Kind: hostclaim.KindServing}}, s, hostwrite.Identity{}), ordinary) // The installation's own work, with instance-control's reader // (productionClaimUnitShapePolicy's). read := func(ctx context.Context, rt registry.Route, s hostbox.Selection) error { return withClaimUnitShape(ctx, rt, s, func(claimUnitBinding) error { return nil }) } check("installation work", claimUnitShapeCheck(t.Context(), route, read, s, true), tc.want) if lookups != 2 { t.Fatal("route lookups", lookups) } }) } } // The agent's reader is root's boundInputs (claimunit_policy.go's agent // branch, over the role-launch socket). Ordinary work never reaches it // without a row; the installation's own work sends it the empty route, which // it passes only when root has no binding and no configured shape. func TestClaimUnitShapePolicyFreshInstallAgentReader(t *testing.T) { var seen []registry.Route route := func(string) (*registry.Route, error) { return nil, nil } read := func(_ context.Context, rt registry.Route, _ hostbox.Selection) error { seen = append(seen, rt) return nil } s := hostbox.Selection{Host: "shop.example"} if err := claimUnitShapePolicy(route, read)(t.Context(), nil, s, hostwrite.Identity{}); !errors.Is(err, hostbox.ErrChanged) || len(seen) != 0 { t.Fatal("ordinary work without a row:", err, seen) } if err := claimUnitShapeCheck(t.Context(), route, read, s, true); err != nil || len(seen) != 1 || seen[0] != (registry.Route{Host: "shop.example"}) { t.Fatal(err, seen) } for _, tc := range []struct { name string binding, shape bool want error }{ {"no binding, no shape", false, false, nil}, {"configured shape", false, true, os.ErrPermission}, {"saved binding", true, true, os.ErrPermission}, } { t.Run(tc.name, func(t *testing.T) { b, in, shape := plan14UnitBindingFixture(t, "console.service", "", "") if tc.binding { if _, err := b.establish(t.Context(), in.Host); err != nil { t.Fatal(err) } } if !tc.shape { b.canonical = func(context.Context, string) (claimUnitShape, error) { return claimUnitShape{}, os.ErrNotExist } } else { b.canonical = func(context.Context, string) (claimUnitShape, error) { return shape, nil } } h := &ClaimLaunchRoot{unitBindings: b, unitProgram: func(claimUnitShape) (claimUnitProgram, error) { return claimUnitProgram{}, nil }} inputs, err := h.boundInputs(t.Context(), registry.Route{Host: in.Host}) if tc.want == nil && (err != nil || inputs != nil) { t.Fatal(inputs, err) } if tc.want != nil && !errors.Is(err, tc.want) { t.Fatalf("got %v, want %v", err, tc.want) } }) } }