package main // The ship-time capability declaration surface (cli.go): the one door an // app's capability block gets into a release manifest through, and the // refusals a human meets at their own terminal rather than on a box an hour // later. import ( "context" "crypto/ed25519" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/caps" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // writeCapsFile drops a capability block at a temp path and returns it. func writeCapsFile(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "capabilities.json") if err := os.WriteFile(p, []byte(body), 0o644); err != nil { t.Fatal(err) } return p } // TestReadCapabilitiesRefusals: everything the strict decoder and // ValidCapabilities refuse is refused HERE, before resolveMode, before a byte // crosses any wire and before release.Ship is called — and every refusal // names the offending thing, because "invalid capabilities" tells an operator // nothing about which line of their block to fix. func TestReadCapabilitiesRefusals(t *testing.T) { cases := []struct { name string body string names string }{ {"unknown capability name", `{"v":1,"daemon":true,"gpu":true}`, "gpu"}, {"unsupported block version", `{"v":2,"daemon":true}`, "update the platform binary"}, {"missing v", `{"daemon":true}`, `"v"`}, {"not JSON at all", `not json`, "capabilities"}, // The file holds EXACTLY the capabilities object — handing it a whole // manifest is refused (on the missing "v", which is checked before // the unknown-name sweep), never half-decoded into something. {"a whole manifest instead of the block", `{"schema":1,"app":"keymail"}`, `"v"`}, {"non-privileged port", `{"v":1,"ports":[{"port":8080}]}`, "8080"}, {"reserved port", `{"v":1,"ports":[{"port":22}]}`, "22"}, {"reserved runtime_dir", `{"v":1,"runtime_dir":"carlos"}`, "carlos"}, {"runtime_dir traversal", `{"v":1,"runtime_dir":"../etc"}`, "runtime_dir"}, {"dbs with replicate app", `{"v":1,"data":{"replicate":"app","dbs":["x.db"]}}`, "dbs"}, // An explicit null is refused rather than folded into "no // capabilities": a human who passed the flag meant to declare // something, and silently shipping nothing is config-that-lies. {"explicit null", `null`, "omit the flag"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got, err := readCapabilities(writeCapsFile(t, c.body), "keymail") if err == nil { t.Fatalf("accepted %s, got %+v", c.body, got) } if !strings.Contains(err.Error(), c.names) { t.Fatalf("refusal %q does not contain %q", err, c.names) } }) } if _, err := readCapabilities(filepath.Join(t.TempDir(), "nope.json"), "keymail"); err == nil { t.Fatal("a missing --capabilities file must be an error, not a silent nil") } // The app name reaches the gate: host_role derives a system user from it, // and an app name too long for one is refused at ship, where a human can // rename, rather than on a box where nobody can. long := strings.Repeat("a", 30) if _, err := readCapabilities(writeCapsFile(t, `{"v":1,"host_role":true}`), long); err == nil { t.Fatal("host_role on an over-long app name must be refused at ship") } } // TestReadCapabilitiesHappy: no flag means nil (and therefore a manifest // byte-identical to yesterday's), and a valid block decodes whole. func TestReadCapabilitiesHappy(t *testing.T) { got, err := readCapabilities("", "keymail") if err != nil || got != nil { t.Fatalf("no --capabilities = (%+v, %v), want (nil, nil)", got, err) } got, err = readCapabilities(writeCapsFile(t, `{"v":1,"command":"relay","daemon":true,"children":true,"runtime_dir":"keymail",`+ `"ports":[{"port":25,"name":"mx"}],"data":{"replicate":"app"},"host_role":true}`), "keymail") if err != nil { t.Fatalf("readCapabilities: %v", err) } if got.V != 1 || got.Command != "relay" || !got.Daemon || !got.Children || got.RuntimeDir != "keymail" || !got.HostRole { t.Fatalf("capabilities = %+v, want the declared block", got) } if len(got.Ports) != 1 || got.Ports[0].Port != 25 || got.Ports[0].Name != "mx" { t.Fatalf("ports = %+v, want [{25 mx}]", got.Ports) } if got.Data == nil || got.Data.Replicate != caps.DataReplicateApp { t.Fatalf("data = %+v, want replicate=app", got.Data) } } // TestShipDirectModeEmbedsTheCapabilityFile is the end-to-end direct-mode // path: the flag's file lands in the immutable manifest in the bucket, and — // the half that matters more — a ship WITHOUT the flag writes a manifest with // no "capabilities" key at all. That second assertion is what keeps every // re-ship of a pre-capabilities release idempotent: release.Ship's // putImmutable compares bytes, so a manifest that grew an empty object would // turn a harmless repeat into a permanent immutable-object conflict. func TestShipDirectModeEmbedsTheCapabilityFile(t *testing.T) { clearDeploymentEnv(t) dir := t.TempDir() t.Setenv("CARLOS_DEPLOYMENT_DIR", dir) bin := filepath.Join(t.TempDir(), "carlos") if err := os.WriteFile(bin, []byte("binary bytes"), 0o755); err != nil { t.Fatal(err) } capsFile := writeCapsFile(t, `{"v":1,"daemon":true,"data":{"replicate":"platform","dbs":["console.db"]}}`) captureStdout(t) runShip([]string{"-app", "console", "-version", "abc1234", "-target", "linux-arm64", "-capabilities", capsFile, bin}) runShip([]string{"-app", "console", "-version", "def5678", "-target", "linux-arm64", bin}) ctx := context.Background() store := release.NewDirStore(dir) m, err := release.GetManifest(ctx, store, release.OpsAccount, "console", "abc1234") if err != nil { t.Fatalf("manifest: %v", err) } if m.Capabilities == nil || !m.Capabilities.Daemon { t.Fatalf("capabilities = %+v, want the declared block", m.Capabilities) } if d := m.Capabilities.Data; d == nil || d.Replicate != caps.DataReplicatePlatform || len(d.DBs) != 1 || d.DBs[0] != "console.db" { t.Fatalf("data = %+v, want platform replication of console.db", d) } raw, _, err := store.Get(ctx, release.ManifestKey(release.OpsAccount, "console", "def5678")) if err != nil { t.Fatalf("read the flagless manifest: %v", err) } if strings.Contains(string(raw), "capabilities") { t.Fatalf("a ship with no --capabilities grew a key:\n%s", raw) } } // tinyProgramForCLI is internal/release's tinyProgram, copied because a test // helper cannot be imported across packages: a stripped linux/arm64 Go binary // whose main package is modPath, built at test time and written to a file // `carlos ship` can read. Nothing is checked in. func tinyProgramForCLI(t *testing.T, modPath string) string { t.Helper() if testing.Short() { t.Skip("builds a binary") } dir := t.TempDir() must := func(err error) { t.Helper() if err != nil { t.Fatal(err) } } must(os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+modPath+"\n\ngo 1.22\n"), 0o644)) must(os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644)) out := filepath.Join(dir, "bin") cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w", "-o", out, ".") cmd.Dir = dir cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64", "CGO_ENABLED=0", "GOFLAGS=-buildvcs=false") if b, err := cmd.CombinedOutput(); err != nil { t.Fatalf("build %s: %v\n%s", modPath, err, b) } return out } // pinShipClock fixes direct-mode ship's BuiltAt so a re-ship of the same // version can be byte-identical to its first ship. func pinShipClock(t *testing.T) { t.Helper() at := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) orig := shipNow shipNow = func() time.Time { return at } t.Cleanup(func() { shipNow = orig }) } // TestShipDirectModeRecordsTheProgramAndTheFlag: the direct-bucket door // stamps the program from the artifact bytes, and --program-change reaches // the manifest. func TestShipDirectModeRecordsTheProgramAndTheFlag(t *testing.T) { clearDeploymentEnv(t) dir := t.TempDir() t.Setenv("CARLOS_DEPLOYMENT_DIR", dir) console, cli := tinyProgramForCLI(t, "example.com/console"), tinyProgramForCLI(t, "example.com/carlos") captureStdout(t) runShip([]string{"-app", "console", "-version", "va", "-target", "linux-arm64", console}) runShip([]string{"-app", "console", "-version", "vb", "-target", "linux-arm64", "-program-change", cli}) store := release.NewDirStore(dir) for ver, want := range map[string]struct { prog string change bool }{"va": {"example.com/console", false}, "vb": {"example.com/carlos", true}} { m, err := release.GetManifest(context.Background(), store, release.OpsAccount, "console", ver) if err != nil || m.Program != want.prog || m.ProgramChange != want.change { t.Fatalf("%s manifest = %+v, %v; want program %s, program_change %v", ver, m, err, want.prog, want.change) } } } // TestShipDirectModeReshipIsStampedLikeTheFirstShip (ruling G2): re-shipping // a version that is already on record skips the program comparison but still // stamps Program and ProgramChange, so release.Ship's byte comparison finds // the stored manifest identical. Before G2 the re-ship's manifest had neither // field and was refused as different content. vb is re-shipped after va2 // took the app back to the console program, which the comparison would // refuse without the flag; the flag is given again, as it was the first time. func TestShipDirectModeReshipIsStampedLikeTheFirstShip(t *testing.T) { clearDeploymentEnv(t) dir := t.TempDir() t.Setenv("CARLOS_DEPLOYMENT_DIR", dir) pinShipClock(t) console, cli := tinyProgramForCLI(t, "example.com/console"), tinyProgramForCLI(t, "example.com/carlos") captureStdout(t) runShip([]string{"-app", "console", "-version", "va", "-target", "linux-arm64", console}) runShip([]string{"-app", "console", "-version", "vb", "-target", "linux-arm64", "-program-change", cli}) runShip([]string{"-app", "console", "-version", "va2", "-target", "linux-arm64", "-program-change", console}) // The re-ships: log.Fatal (a conflict) would end the test binary here. runShip([]string{"-app", "console", "-version", "va", "-target", "linux-arm64", console}) runShip([]string{"-app", "console", "-version", "vb", "-target", "linux-arm64", "-program-change", cli}) m, err := release.GetManifest(context.Background(), release.NewDirStore(dir), release.OpsAccount, "console", "vb") if err != nil || m.Program != "example.com/carlos" || !m.ProgramChange { t.Fatalf("vb manifest = %+v, %v; want the first ship's stamp kept", m, err) } } // --- the grant door (Task 4) ------------------------------------------- // // `carlos route -grant/-revoke` is the operator's door to a transfer of box // authority, so its refusals are pinned as pure predicates: everything below // is decided before the registry is opened for writing. // TestGrantRailRefusals: every structural reason a grant or a revoke is // refused from the row alone. The two row-shape refusals apply to a REVOKE // as well — a row this verb does not own is not a row it should be rewriting // in either direction. func TestGrantRailRefusals(t *testing.T) { unit := registry.Route{Host: "keymail.dev", Kind: "instance", Backing: registry.BackingUnit, App: "keymail"} cases := []struct { name string rt registry.Route tok string says string }{ {"reserved port", unit, "port:443", "reserved"}, {"high port", unit, "port:8080", "1..1023"}, {"nonsense token", unit, "gpu", "unknown grant token"}, {"platform+canary row", func() registry.Route { rt := unit rt.Platform, rt.Canary = true, true return rt }(), "port:25", "does not own"}, {"exec-child backing", func() registry.Route { rt := unit rt.Backing = "" return rt }(), "port:25", "unit-backed instance routes"}, {"service route", func() registry.Route { rt := unit rt.Kind = "service" return rt }(), "host-role", "unit-backed instance routes"}, {"static route", func() registry.Route { rt := unit rt.Kind = "static" rt.Backing = "" return rt }(), "port:25", "unit-backed instance routes"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := grantRail(c.rt, c.tok) if err == nil { t.Fatalf("grantRail(%+v, %q) accepted; want a refusal", c.rt, c.tok) } if !strings.Contains(err.Error(), c.says) { t.Fatalf("refusal %q does not say %q", err, c.says) } }) } for _, tok := range []string{"port:25", "host-role", "doc-engine"} { if err := grantRail(unit, tok); err != nil { t.Fatalf("grantRail(unit-backed instance, %q) = %v, want accepted", tok, err) } } // A plain Platform apex (not Canary) is grantable: nothing rebuilds that // row from derived values the way the canary-minting pass does. apex := unit apex.Platform = true if err := grantRail(apex, "port:25"); err != nil { t.Fatalf("grantRail(platform apex) = %v, want accepted", err) } } // TestPortGrantConflict: a privileged port can be held by exactly one process // on one machine, so granting one already granted elsewhere on this box is // refused NAMING the other host — the operator-facing half of the converge // pass's own claim check, which otherwise would not run until an hour after // the typing. func TestPortGrantConflict(t *testing.T) { routes := []registry.Route{ {Host: "keymail.dev", Caps: "host-role,port:25"}, {Host: "console.example.com", Caps: "host-role"}, {Host: "plain.example.com"}, } err := portGrantConflict(routes, "other.example.com", "port:25") if err == nil { t.Fatal("granting a port another route already holds must be refused") } if !strings.Contains(err.Error(), "keymail.dev") { t.Fatalf("refusal %q does not name the route already holding the port", err) } // Re-granting to the route that already holds it is not a conflict with // itself — that path is the idempotent no-op, not a refusal. if err := portGrantConflict(routes, "keymail.dev", "port:25"); err != nil { t.Fatalf("re-granting to the holder = %v, want no conflict", err) } // A different port is free. if err := portGrantConflict(routes, "other.example.com", "port:465"); err != nil { t.Fatalf("port:465 = %v, want no conflict", err) } // host-role is never conflict-checked: the box role is not consumed by // being used, and two apps holding it is two operator decisions. if err := portGrantConflict(routes, "other.example.com", "host-role"); err != nil { t.Fatalf("host-role = %v, want no conflict even though another route holds it", err) } // doc-engine is never conflict-checked either, and for a sharper reason: // it is per-route unit text, not a box resource, and a dedicated sidecar // box running several engine routes is the expected shape. engineRoutes := []registry.Route{{Host: "gotenberg.docs.example.com", Caps: "doc-engine"}} if err := portGrantConflict(engineRoutes, "ocr.docs.example.com", "doc-engine"); err != nil { t.Fatalf("doc-engine = %v, want no conflict even though another route holds it", err) } // A row whose caps column does not parse fails CLOSED, naming that row: // "I could not find out whether this port is claimed" is not a licence. bad := []registry.Route{{Host: "handedited.example.com", Caps: "port:25,gpu"}} err = portGrantConflict(bad, "other.example.com", "port:25") if err == nil { t.Fatal("an unreadable caps column on another row must fail the conflict check closed") } if !strings.Contains(err.Error(), "handedited.example.com") { t.Fatalf("refusal %q does not name the unreadable row", err) } } // TestRouteDaemonCheck pins the tri-state the -hibernate rail reads: a proven // daemon, a proven non-daemon, and — the case that matters most — every way // the comparison can fail to resolve, each of which reports UNPROVEN so the // caller refuses rather than guesses. `carlos route` refuses on unproven; // `carlos add` (provision time, no pubkey, no store, no release) proceeds on // it, which is why the two are separated here rather than folded into a bool. func TestRouteDaemonCheck(t *testing.T) { ctx := context.Background() dir := t.TempDir() store := release.NewDirStore(dir) pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } ship := func(app, version string, daemon bool) { t.Helper() m := release.Manifest{App: app, Version: version, Kind: "binary", Artifacts: map[string]release.Artifact{"linux-arm64": {}}} if daemon { m.Capabilities = &caps.Set{V: 1, Daemon: true} } if err := release.Ship(ctx, store, release.OpsAccount, m, map[string][]byte{"linux-arm64": []byte("bytes " + version)}); err != nil { t.Fatalf("ship %s %s: %v", app, version, err) } } ship("keymail", "abc1234", true) ship("kass", "def5678", false) ship("kass", "0badbad", true) if err := release.Promote(ctx, store, priv, release.OpsAccount, "keymail", "canary/keymail", "abc1234"); err != nil { t.Fatal(err) } if err := release.Promote(ctx, store, priv, release.OpsAccount, "kass", "canary/kass", "def5678"); err != nil { t.Fatal(err) } keymail := registry.Route{Host: "keymail.dev", App: "keymail", Channel: "canary/keymail"} daemon, note, proven := routeDaemonCheck(ctx, store, pub, release.OpsAccount, keymail) if !daemon || !proven { t.Fatalf("a declared daemon = (%v, %q, %v), want proven daemon", daemon, note, proven) } kass := registry.Route{Host: "kass.example.com", App: "kass", Channel: "canary/kass"} daemon, note, proven = routeDaemonCheck(ctx, store, pub, release.OpsAccount, kass) if daemon || !proven { t.Fatalf("a release with no daemon = (%v, %q, %v), want proven non-daemon", daemon, note, proven) } // BOTH versions count as "what this route runs": the one it serves now, // and the one its channel points at (what the next adopt converges onto). // A route still serving a daemon build may not hibernate just because the // channel has since moved to one that isn't. stale := kass stale.Version = "0badbad" daemon, note, proven = routeDaemonCheck(ctx, store, pub, release.OpsAccount, stale) if !daemon || !proven { t.Fatalf("a route still serving a daemon build = (%v, %q, %v), want proven daemon", daemon, note, proven) } // A route naming no app is PROVEN not a daemon: there is no release, so // there is nothing that could declare anything. daemon, _, proven = routeDaemonCheck(ctx, store, pub, release.OpsAccount, registry.Route{Host: "static.example.com"}) if daemon || !proven { t.Fatalf("a route with no app = (%v, %v), want a proven non-daemon", daemon, proven) } // The four unproven shapes. Each must report proven=false, so `carlos // route -hibernate` refuses: "I could not find out" is never a licence. unproven := []struct { name string store release.Store pub ed25519.PublicKey rt registry.Route }{ {"no pubkey", store, nil, keymail}, {"no store", nil, pub, keymail}, {"unresolvable channel", store, pub, registry.Route{Host: "keymail.dev", App: "keymail", Channel: "canary/nope"}}, // The channel resolves and its manifest declares no daemon, but the // version the route is actually SERVING has no readable manifest — // so half the question is unanswered, and half an answer is none. {"manifest missing for the served version", store, pub, func() registry.Route { rt := kass rt.Version = "9999999" return rt }()}, } for _, c := range unproven { t.Run(c.name, func(t *testing.T) { _, note, proven := routeDaemonCheck(ctx, c.store, c.pub, release.OpsAccount, c.rt) if proven { t.Fatalf("%s reported a proven answer (%q); want unproven so the caller refuses", c.name, note) } if note == "" { t.Fatal("an unproven verdict must carry a note saying what could not be found out") } }) } // A wrong key proves nothing either: the pointer's signature must verify. otherPub, _, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } if _, _, proven := routeDaemonCheck(ctx, store, otherPub, release.OpsAccount, keymail); proven { t.Fatal("a pointer signed by another key must not prove anything") } } // TestBackingRailRefusals: `carlos route -backing` is the migration door // (app-capabilities design §5b/§5c), and its two row-shape refusals are // structural — no flag overrides either. It is STRICTER than the channel and // environment axes on platform rows: those refuse only Platform+Canary // (rebuilt by the reconciler), this refuses a plain platform apex too, // because a second carlos-app@ unit under an alias would claim ports and a // state directory the real route already holds. func TestBackingRailRefusals(t *testing.T) { instance := registry.Route{Host: "console.example.com", Kind: "instance", App: "console"} cases := []struct { name string rt registry.Route says string }{ {"platform apex", func() registry.Route { rt := instance rt.Platform = true return rt }(), "does not own"}, {"platform+canary row", func() registry.Route { rt := instance rt.Platform, rt.Canary = true, true return rt }(), "does not own"}, {"service route", func() registry.Route { rt := instance rt.Kind = "service" return rt }(), "only meaningful for an instance route"}, {"static route", func() registry.Route { rt := instance rt.Kind = "static" return rt }(), "only meaningful for an instance route"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // Row-shape refusals hold whichever value is being written, so // both targets are exercised. for _, target := range []string{registry.BackingUnit, ""} { err := backingRail(c.rt, target) if err == nil { t.Fatalf("backingRail(%+v, %q) accepted; want a refusal", c.rt, target) } if !strings.Contains(err.Error(), c.says) { t.Fatalf("refusal %q does not say %q", err, c.says) } } }) } // Both directions of the migration are the same verb on the same row, so // both must clear the rail from either starting value. if err := backingRail(instance, registry.BackingUnit); err != nil { t.Fatalf("backingRail(exec-backed instance) = %v, want accepted", err) } unit := instance unit.Backing = registry.BackingUnit if err := backingRail(unit, ""); err != nil { t.Fatalf("backingRail(unit-backed instance) = %v, want accepted", err) } } // The pair rail, from backingRail's side: `carlos route -unit X` followed by // `carlos route -backing unit` reaches exactly the state unitRail forbids in // the other order, and a rail guarded at one end only is not guarded. The // refusal is DIRECTIONAL — moving a stamped row AWAY from unit backing stays // legal, or a row that somehow reached the forbidden state would have no way // back out. func TestBackingRailRefusesUnitBackingOnAStampedRow(t *testing.T) { stamped := registry.Route{ Host: "console.carloku.com", Kind: "instance", App: "console", Addr: "127.0.0.1:8090", Unit: "console.service", } err := backingRail(stamped, registry.BackingUnit) if err == nil { t.Fatal("backingRail(unit-stamped row, backing=unit) accepted; want a refusal") } // The operator has to be told which flag to undo, or the refusal is a // dead end. if !strings.Contains(err.Error(), "--unit -") { t.Errorf("refusal %q does not name the clear command", err) } if err := backingRail(stamped, ""); err != nil { t.Fatalf("backingRail(unit-stamped row, backing=\"\") = %v, want accepted — the way out must stay open", err) } } // TestUnitRailRefusals: `carlos route -unit` is the stamp issue #110 needed, // and its one structural refusal is a unit-backed route — that route's // process is the carlos-app@ unit the platform derives and already // restarts, so a second unit name on the row is two answers to one question. // // Everything else clears the rail on purpose, and the omissions are the // interesting half: unlike backingRail this one does NOT refuse Platform rows // (the domains reconciler never derives this column and Upsert preserves it, // so a stamp survives a re-mint) and does NOT refuse non-instance kinds (a // `service` route is exactly a sibling process the platform only proxies, and // naming the unit that owns it is a true statement). func TestUnitRailRefusals(t *testing.T) { console := registry.Route{ Host: "console.carloku.com", Kind: "instance", App: "console", Addr: "127.0.0.1:8090", } unitBacked := console unitBacked.Backing = registry.BackingUnit err := unitRail(unitBacked) if err == nil { t.Fatal("unitRail(unit-backed route) accepted; want a refusal") } if !strings.Contains(err.Error(), "carlos-app@") { t.Fatalf("refusal %q does not name the unit that already owns the process", err) } for _, c := range []struct { name string rt registry.Route }{ {"addr-backed console", console}, {"platform apex", func() registry.Route { rt := console; rt.Platform = true; return rt }()}, {"platform+canary row", func() registry.Route { rt := console rt.Platform, rt.Canary = true, true return rt }()}, {"service route", func() registry.Route { rt := console; rt.Kind = "service"; return rt }()}, } { t.Run(c.name, func(t *testing.T) { if err := unitRail(c.rt); err != nil { t.Fatalf("unitRail(%+v) = %v, want accepted", c.rt, err) } }) } } // TestRouteUnitStampsAnExistingRoute drives the whole door as a subprocess, // because that is the shape the fix ships in: the production consoles already // exist, and `carlos route -unit console.service` has to stamp a LIVE // registry row rather than need the route recreated. It also pins the // refusals an operator can actually type — a typo'd unit name above all, // since a bad name is invisible afterwards (a restart that reaches nothing // looks exactly like one that worked). func TestRouteUnitStampsAnExistingRoute(t *testing.T) { bin := buildCarlos(t) tmp := t.TempDir() scratch := t.TempDir() regPath := filepath.Join(tmp, "registry.db") run := func(args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t) // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } // The console as it exists on a box today: addr-backed, no unit. if out, err := run("add", "-registry", regPath, "-app", "console", "-addr", "127.0.0.1:8090", "console.carloku.com"); err != nil { t.Fatalf("add console route: %v\n%s", err, out) } unitOf := func(host string) string { t.Helper() reg, err := registry.Open(regPath) if err != nil { t.Fatalf("open registry: %v", err) } defer reg.Close() rt, err := reg.Get(host) if err != nil || rt == nil { t.Fatalf("get %s: %+v, %v", host, rt, err) } return rt.Unit } if got := unitOf("console.carloku.com"); got != "" { t.Fatalf("a fresh route already names a unit: %q", got) } // A typo must cost nothing and reach nothing. out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "console.servcie") if err == nil { t.Fatalf("route --unit console.servcie: want non-zero exit, got 0\n%s", out) } if got := unitOf("console.carloku.com"); got != "" { t.Fatalf("a refused --unit still wrote %q", got) } // -dry-run runs the rail and prints the plan, writing nothing. out, err = run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "console.service", "-dry-run") if err != nil { t.Fatalf("route --unit --dry-run: %v\n%s", err, out) } if !strings.Contains(out, "console.service") { t.Errorf("dry run output = %q, want it to name the unit", out) } if got := unitOf("console.carloku.com"); got != "" { t.Fatalf("--dry-run wrote %q", got) } // The real stamp — the command the deploy runs on each box. if out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "console.service"); err != nil { t.Fatalf("route --unit console.service: %v\n%s", err, out) } if got := unitOf("console.carloku.com"); got != "console.service" { t.Fatalf("Unit = %q after the stamp, want console.service", got) } // It survives a re-`carlos add` of the same row (registry.Upsert's // preservation): dropping it would silently restore issue #110. if out, err := run("add", "-registry", regPath, "-app", "console", "-addr", "127.0.0.1:8090", "console.carloku.com"); err != nil { t.Fatalf("re-add console route: %v\n%s", err, out) } if got := unitOf("console.carloku.com"); got != "console.service" { t.Fatalf("Unit = %q after a re-add, want the stamp preserved", got) } // The listing shows it, so an operator can see which routes adoption can // cycle without opening the DB. out, err = run("instances", "-registry", regPath) if err != nil { t.Fatalf("instances: %v\n%s", err, out) } if !strings.Contains(out, "UNIT") || !strings.Contains(out, "console.service") { t.Errorf("instances output = %q, want a UNIT column carrying console.service", out) } // The same listing carries a route's config-ENVIRONMENT binding (issue // #64): it decides which secrets the route's process is delivered, and // reading it used to mean sqlite3 against this file. Bind one and read // it back through the binary. if out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-environment", "relay"); err == nil { // The bind only succeeds where the bundle exists; where it does // not, the refusal is the environment rail doing its job and the // column assertion below still has the unbound case to check. _ = out } out, err = run("instances", "-registry", regPath) if err != nil { t.Fatalf("instances: %v\n%s", err, out) } if !strings.Contains(out, "ENV") { t.Errorf("instances output = %q, want an ENV column — a route's config binding was unreadable without sqlite3 (#64)", out) } // And the clear door, so a mis-stamp is reversible. if out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "-"); err != nil { t.Fatalf("route --unit -: %v\n%s", err, out) } if got := unitOf("console.carloku.com"); got != "" { t.Fatalf("Unit = %q after --unit -, want it cleared", got) } // The pair rail through the real binary, in both orders. Neither door may // mint a row that is unit-BACKED and unit-NAMED at once: the dispatch // honours the name, so the carlos-app@ unit would never be cycled // by adoption again — issue #110 recreated for an ordinary tenant. out, err = run("add", "-registry", regPath, "-app", "kass", "-backing", "unit", "-unit", "bespoke.service", "-db", filepath.Join(tmp, "t.db"), "pair.example.com") if err == nil { t.Fatalf("add --backing unit --unit bespoke.service: want non-zero exit, got 0\n%s", out) } if !strings.Contains(out, "carlos-app@") { t.Errorf("add refusal = %q, want it to name the unit that already owns the process", out) } if out, err := run("add", "-registry", regPath, "-app", "kass", "-backing", "unit", "-db", filepath.Join(tmp, "t.db"), "tenant.example.com"); err != nil { t.Fatalf("add unit tenant: %v\n%s", err, out) } out, err = run("route", "-registry", regPath, "-host", "tenant.example.com", "-unit", "bespoke.service") if err == nil { t.Fatalf("route --unit on a unit-backed route: want non-zero exit, got 0\n%s", out) } if out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "console.service"); err != nil { t.Fatalf("re-stamp console: %v\n%s", err, out) } out, err = run("route", "-registry", regPath, "-host", "console.carloku.com", "-backing", "unit") if err == nil { t.Fatalf("route --backing unit on a unit-stamped route: want non-zero exit, got 0\n%s", out) } if !strings.Contains(out, "--unit -") { t.Errorf("backing refusal = %q, want it to name the clear command", out) } if out, err := run("route", "-registry", regPath, "-host", "console.carloku.com", "-unit", "-"); err != nil { t.Fatalf("clear the stamp again: %v\n%s", err, out) } // The exclusions: each of these is a command that would otherwise do part // of what was typed and report success. for _, c := range []struct { name string args []string }{ {"with --channel", []string{"-host", "console.carloku.com", "-unit", "console.service", "-channel", "stable"}}, {"with --backing", []string{"-host", "console.carloku.com", "-unit", "console.service", "-backing", "unit"}}, {"with --hibernate", []string{"-host", "console.carloku.com", "-unit", "console.service", "-hibernate"}}, {"bulk --app", []string{"-app", "console", "-unit", "console.service"}}, {"empty value", []string{"-host", "console.carloku.com", "-unit", ""}}, {"unknown host", []string{"-host", "nosuch.example.com", "-unit", "console.service"}}, } { t.Run(c.name, func(t *testing.T) { out, err := run(append([]string{"route", "-registry", regPath}, c.args...)...) if err == nil { t.Fatalf("route %v: want non-zero exit, got 0\n%s", c.args, out) } }) } if got := unitOf("console.carloku.com"); got != "" { t.Fatalf("a refused command wrote %q", got) } } // TestAddPreservesUpstreamsOnReAdd carries Task 3's review forward (registry // doc at cli.go:194-198, now closed by runAdd): a bare re-`carlos add` over a // host that already carries a multi-upstream set must not blank it. Upsert's // addrs/health columns are plain overwrites, and until this fix a Route // literal built from add's own flags — none of which name Addrs/Health — // would clobber whatever `carlos instances set-upstreams` (or a converged // reconcile pass) had established, the same shape of hazard a plain // excluded.environment once caused. // // An explicit -addr on that same re-add is its own case (Task 8 review, // IMPORTANT 1): it does NOT silently win by clearing the established Addrs // — that would build a row ValidateShape refuses everywhere else ("addr and // addrs both set") and, even set aside that, would be reverted by the next // converge tick anyway (convergeOwned re-derives Addr from the record's // Addrs). It is refused instead, pointing at `carlos instances // set-upstreams` — the record, not this box verb, owns multi-upstream shape. // // SetUpstreams (internal/registry) is used directly, not through a CLI verb, // to establish the set: it is the narrow setter reconcile itself converges // through, and no `carlos` command exposes it standalone — set-upstreams // (Task 8, instances_cli.go) speaks to a console, and this test wants // nothing else about the write path in flight. func TestAddPreservesUpstreamsOnReAdd(t *testing.T) { bin := buildCarlos(t) tmp := t.TempDir() scratch := t.TempDir() regPath := filepath.Join(tmp, "registry.db") run := func(args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t) // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } const host = "kass.example.com" if out, err := run("add", "-registry", regPath, "-app", "kass", "-addr", "127.0.0.1:9001", host); err != nil { t.Fatalf("add: %v\n%s", err, out) } reg, err := registry.Open(regPath) if err != nil { t.Fatalf("open registry: %v", err) } defer reg.Close() const wantAddrs = "127.0.0.1:9001,127.0.0.1:9002" const wantHealth = `{"path":"/healthz","status":200}` if err := reg.SetUpstreams(host, "", wantAddrs, wantHealth); err != nil { t.Fatalf("SetUpstreams: %v", err) } // Steering (multi-region ingress design Part 4) has the identical gap: // no -steering flag on this verb, Upsert's steering column is a plain // overwrite like addrs/health, so a bare re-add must not blank a route's // Route53 opt-in either (review finding: a re-add landing inside the // steering housekeeper's 5-minute converge window would otherwise delete // the host's live latency records). if err := reg.SetSteering(host, "latency"); err != nil { t.Fatalf("SetSteering: %v", err) } // A re-add naming -addr again, once the row carries an established // multi-upstream set, is refused rather than silently clearing it — the // record owns that shape, not this box verb. out, err := run("add", "-registry", regPath, "-app", "kass", "-addr", "127.0.0.1:9001", host) if err == nil { t.Fatalf("re-add with --addr over an established Addrs set: want non-zero exit, got 0\n%s", out) } if !strings.Contains(out, "carlos instances set-upstreams") { t.Errorf("refusal = %q, want it to name carlos instances set-upstreams", out) } rt, err := reg.Get(host) if err != nil || rt == nil { t.Fatalf("get after the refused re-add: %+v, %v", rt, err) } if rt.Addrs != wantAddrs || rt.Health != wantHealth || rt.Addr != "" { t.Fatalf("row after a refused re-add = addr %q addrs %q health %q, want it untouched (addr \"\", addrs %q, health %q)", rt.Addr, rt.Addrs, rt.Health, wantAddrs, wantHealth) } if rt.Steering != "latency" { t.Fatalf("steering after a refused re-add = %q, want it untouched (latency)", rt.Steering) } // A re-add naming NO -addr at all must not blank Addrs/Health either — // this box-local verb only carries what its own flags set, and the // refusal above is -addr-specific, not a blanket lockout of the host. if out, err := run("add", "-registry", regPath, "-app", "kass", host); err != nil { t.Fatalf("re-add with no --addr: %v\n%s", err, out) } rt, err = reg.Get(host) if err != nil || rt == nil { t.Fatalf("get after flagless re-add: %+v, %v", rt, err) } if rt.Addrs != wantAddrs || rt.Health != wantHealth { t.Errorf("Addrs/Health after a flagless re-add = %q/%q, want both still preserved", rt.Addrs, rt.Health) } if rt.Addr != "" { t.Errorf("Addr after a flagless re-add = %q, want it to stay preserved as the establishing SetUpstreams left it", rt.Addr) } if rt.Steering != "latency" { t.Errorf("steering after a flagless re-add = %q, want it still preserved as the establishing SetSteering left it", rt.Steering) } // The refusal is Addrs-specific: an ordinary re-add with -addr on a host // that has no established multi-upstream set is unaffected and still // works exactly as it always has. const solo = "solo.example.com" if out, err := run("add", "-registry", regPath, "-app", "kass", "-addr", "127.0.0.1:9100", solo); err != nil { t.Fatalf("add solo: %v\n%s", err, out) } if out, err := run("add", "-registry", regPath, "-app", "kass", "-addr", "127.0.0.1:9200", solo); err != nil { t.Fatalf("re-add solo with --addr (no established Addrs set): %v\n%s", err, out) } soloRt, err := reg.Get(solo) if err != nil || soloRt == nil { t.Fatalf("get solo: %+v, %v", soloRt, err) } if soloRt.Addr != "127.0.0.1:9200" { t.Errorf("solo Addr = %q, want the second --addr to have won", soloRt.Addr) } } // --- release versioning: the CLI surface ------------------------------- // // (release versioning spec, 2026-08-08.) Driven through the real binary in // DIRECT mode, following scopedkeys_e2e_test.go exactly: a DirStore // deployment dir under t.TempDir(), a root signing key the binary mints for // itself, carlosTestEnv to mask the operator's real login and console, and // cmd.Dir at a scratch dir because the binary walks cwd ancestors for // .carlos/config. // versioningCLI builds carlos, mints a root key with it, and returns a // runner bound to a fresh deployment dir — returned alongside the runner so // a test can assert on what actually landed in the store, not only on what // stdout claimed. func versioningCLI(t *testing.T) (run func(args ...string) (string, error), bucket string) { t.Helper() bin := buildCarlos(t) bucket = t.TempDir() scratch := t.TempDir() store := []string{"CARLOS_DEPLOYMENT_DIR=" + bucket, "CARLOS_DATA_DIR=" + t.TempDir()} runWith := func(env []string, args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t, append(append([]string{}, store...), env...)...) // pick() walks cwd ancestors for .carlos/config, so the subprocess must not run from the real checkout. cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } out, err := runWith(nil, "release-keygen") if err != nil { t.Fatalf("release-keygen: %v\n%s", err, out) } keys := []string{ "CARLOS_RELEASE_KEY=" + envValue(t, out, "CARLOS_RELEASE_KEY"), "CARLOS_RELEASE_PUBKEY=" + envValue(t, out, "CARLOS_RELEASE_PUBKEY"), } return func(args ...string) (string, error) { return runWith(keys, args...) }, bucket } // versioningArtifact drops one throwaway binary artifact and returns its path. func versioningArtifact(t *testing.T, body string) string { t.Helper() p := filepath.Join(t.TempDir(), "kass") if err := os.WriteFile(p, []byte(body), 0o755); err != nil { t.Fatal(err) } return p } // TestShipMintsFromTargetDirect: with a target set, a -version-less ship // mints the next iteration under it, and the label/notes the operator typed // reach the manifest — the two halves of the feature an operator sees at // their own terminal. func TestShipMintsFromTargetDirect(t *testing.T) { run, bucket := versioningCLI(t) if out, err := run("version", "target", "-app", "kass", "0.5.0"); err != nil { t.Fatalf("version target 0.5.0: %v\n%s", err, out) } out, err := run("ship", "-app", "kass", "-label", "fixing modals", "-notes", "modals close on escape now", versioningArtifact(t, "build one")) if err != nil { t.Fatalf("minted ship: %v\n%s", err, out) } if !strings.Contains(out, "0.5.0-1") { t.Fatalf("the first minted ship does not name 0.5.0-1:\n%s", out) } st := release.NewDirStore(bucket) m, err := release.GetManifest(context.Background(), st, release.OpsAccount, "kass", "0.5.0-1") if err != nil { t.Fatalf("manifest 0.5.0-1: %v", err) } if m.Label != "fixing modals" { t.Fatalf("manifest label = %q, want %q", m.Label, "fixing modals") } if m.Notes != "modals close on escape now" { t.Fatalf("manifest notes = %q, want the --notes text", m.Notes) } // The counter comes off what has been SHIPPED, so a second ship under the // same target is -2 with no state anywhere but the release dirs. out, err = run("ship", "-app", "kass", "-label", "and the sidebar", versioningArtifact(t, "build two")) if err != nil { t.Fatalf("second minted ship: %v\n%s", err, out) } if !strings.Contains(out, "0.5.0-2") { t.Fatalf("the second minted ship does not name 0.5.0-2:\n%s", out) } } // TestShipNoTargetNoVersionFatal: -version is optional only where a target // makes it inferable. With neither, the refusal has to name the verb that // fixes it — an operator who just learned -version is optional must not be // left guessing what the alternative was. func TestShipNoTargetNoVersionFatal(t *testing.T) { run, _ := versioningCLI(t) out, err := run("ship", "-app", "kass", versioningArtifact(t, "build one")) if err == nil { t.Fatalf("ship with no --version and no target succeeded:\n%s", out) } if !strings.Contains(out, "carlos version target") { t.Fatalf("refusal does not name `carlos version target`:\n%s", out) } } // TestVersionTargetGetSetDirect: get before set, set, get after — and a // garbage target refused rather than stored. func TestVersionTargetGetSetDirect(t *testing.T) { run, _ := versioningCLI(t) out, err := run("version", "target", "-app", "kass") if err != nil { t.Fatalf("version target (unset): %v\n%s", err, out) } if !strings.Contains(out, "no version target set") { t.Fatalf("an unset target does not say so:\n%s", out) } if out, err := run("version", "target", "-app", "kass", "0.5.0"); err != nil { t.Fatalf("version target 0.5.0: %v\n%s", err, out) } out, err = run("version", "target", "-app", "kass") if err != nil { t.Fatalf("version target (set): %v\n%s", err, out) } if !strings.Contains(out, "0.5.0") { t.Fatalf("version target does not print the stored target:\n%s", out) } if out, err := run("version", "target", "-app", "kass", "banana"); err == nil { t.Fatalf("version target accepted a non-semver:\n%s", out) } // `carlos version` on its own is still the build id, not a target: the // new sub-verb must not have moved the oldest output in the binary. out, err = run("version") if err != nil { t.Fatalf("version: %v\n%s", err, out) } if !strings.HasPrefix(out, "carlos ") || strings.Contains(out, "version target") { t.Fatalf("`carlos version` is no longer the build id:\n%s", out) } } // TestReleasesTableDirect: the shipped list, newest first, carrying the // labels that were typed at ship time. func TestReleasesTableDirect(t *testing.T) { run, _ := versioningCLI(t) if out, err := run("version", "target", "-app", "kass", "0.5.0"); err != nil { t.Fatalf("version target: %v\n%s", err, out) } if out, err := run("ship", "-app", "kass", "-label", "fixing modals", versioningArtifact(t, "build one")); err != nil { t.Fatalf("ship one: %v\n%s", err, out) } if out, err := run("ship", "-app", "kass", "-label", "and the sidebar", versioningArtifact(t, "build two")); err != nil { t.Fatalf("ship two: %v\n%s", err, out) } out, err := run("releases", "-app", "kass") if err != nil { t.Fatalf("releases: %v\n%s", err, out) } lines := strings.Split(strings.TrimRight(out, "\n"), "\n") if len(lines) < 3 { t.Fatalf("releases printed %d lines, want a header and two rows:\n%s", len(lines), out) } if !strings.Contains(lines[0], "VERSION") || !strings.Contains(lines[0], "LABEL") || !strings.Contains(lines[0], "TAG") { t.Fatalf("releases header = %q, want VERSION/LABEL/.../TAG", lines[0]) } if !strings.Contains(lines[1], "0.5.0-2") || !strings.Contains(lines[1], "and the sidebar") { t.Fatalf("the newest release is not the first row:\n%s", out) } if !strings.Contains(out, "fixing modals") { t.Fatalf("releases does not carry the older label:\n%s", out) } } // TestPromoteStableTagOutputDirect: the tag a stable promotion cuts is // PRINTED — the operator who typed the promote is the one who needs to know // what the release is now called. `carlos channels` carries it too, so the // name survives past the one line that announced it. func TestPromoteStableTagOutputDirect(t *testing.T) { run, _ := versioningCLI(t) if out, err := run("version", "target", "-app", "kass", "0.5.0"); err != nil { t.Fatalf("version target: %v\n%s", err, out) } if out, err := run("ship", "-app", "kass", "-label", "fixing modals", versioningArtifact(t, "build one")); err != nil { t.Fatalf("ship: %v\n%s", err, out) } for _, ch := range []string{"edge", "beta"} { if out, err := run("promote", "-app", "kass", "0.5.0-1", ch); err != nil { t.Fatalf("promote to %s: %v\n%s", ch, err, out) } } out, err := run("promote", "-app", "kass", "0.5.0-1", "stable") if err != nil { t.Fatalf("promote to stable: %v\n%s", err, out) } if !strings.Contains(out, "tagged 0.5.0") { t.Fatalf("a stable promotion does not print the tag it cut:\n%s", out) } chans, err := run("channels", "-app", "kass") if err != nil { t.Fatalf("channels: %v\n%s", err, chans) } if !strings.Contains(chans, "TAG") { t.Fatalf("channels has no TAG column:\n%s", chans) } var stableRow string for _, line := range strings.Split(chans, "\n") { if strings.HasPrefix(line, "stable") { stableRow = strings.TrimRight(line, " ") } } if stableRow == "" { t.Fatalf("channels has no stable row:\n%s", chans) } if !strings.Contains(stableRow, "0.5.0-1") || !strings.HasSuffix(stableRow, "0.5.0 "+textStateNo) { t.Fatalf("the stable row does not carry the tag of the version it serves: %q\n%s", stableRow, chans) } } // TestNotesFileAndNotesConflict: two spellings of one field, so passing both // is a refusal rather than a silent precedence rule nobody can remember. func TestNotesFileAndNotesConflict(t *testing.T) { run, _ := versioningCLI(t) notes := filepath.Join(t.TempDir(), "NOTES.md") if err := os.WriteFile(notes, []byte("from a file"), 0o644); err != nil { t.Fatal(err) } out, err := run("ship", "-app", "kass", "-version", "9f3c1ad", "-notes", "inline", "-notes-file", notes, versioningArtifact(t, "build one")) if err == nil { t.Fatalf("ship accepted both --notes and --notes-file:\n%s", out) } if !strings.Contains(out, "-notes-file") { t.Fatalf("refusal does not name --notes-file:\n%s", out) } } // pickPromoteChannel is what makes `carlos promote ` legal without a // channel. The rule it has to hold is not "guess well" but "resolve when the // answer is unambiguous, and otherwise say what this app actually has" — // skills#29 went wrong at the second half: --help documented a four-rung // ladder, the app had two channels of its own, and the guess landed on a name // the app did not have. func TestPickPromoteChannel(t *testing.T) { // One channel is the shape of most apps: a new app is born with exactly // one, and that one is its production. if ch, err := pickPromoteChannel(apiChannelsResult{Channels: []apiChannel{{Channel: "edge"}}}, "a"); err != nil || ch != "edge" { t.Fatalf("one channel: %q %v", ch, err) } // The reported app: a canary someone cut this morning beside the one real // channel. canary/* is a reserved per-session dead end outside any // pipeline, so it must not turn an ordinary promote into an ambiguity. got, err := pickPromoteChannel(apiChannelsResult{Channels: []apiChannel{ {Channel: "canary"}, {Channel: "edge"}, }}, "techarena") if err != nil || got != "edge" { t.Fatalf("canary beside edge: %q %v — a canary must not make the real channel ambiguous", got, err) } // A pipeline's DECLARED list wins over the promoted rows: a channel with // no pointer yet is still one you may promote onto. if ch, err := pickPromoteChannel(apiChannelsResult{ PipelineChannels: []string{"review"}, PipelineEntry: "review", Channels: []apiChannel{{Channel: "edge"}}, }, "a"); err != nil || ch != "review" { t.Fatalf("one-channel pipeline: %q %v", ch, err) } // Several: refuse, and NAME them. This is the half the reporter needed // and the reason a bare default would be the wrong fix — a promote that // silently picked one of these could ship to production. _, err = pickPromoteChannel(apiChannelsResult{ PipelineChannels: []string{"edge", "beta", "stable"}, PipelineEntry: "edge", }, "a") if err == nil { t.Fatal("a multi-channel pipeline must refuse, not pick") } for _, want := range []string{"edge", "beta", "stable", "entry channel is edge"} { if !strings.Contains(err.Error(), want) { t.Errorf("refusal %q does not name %q", err, want) } } _, err = pickPromoteChannel(apiChannelsResult{Channels: []apiChannel{ {Channel: "edge"}, {Channel: "production"}, }}, "a") if err == nil { t.Fatal("two channels must refuse, not pick") } if !strings.Contains(err.Error(), "edge, production") { t.Errorf("refusal %q does not list the app's own channels", err) } // Nothing to resolve from is its own sentence, not a confusing "0 channels". if _, err := pickPromoteChannel(apiChannelsResult{}, "a"); err == nil || !strings.Contains(err.Error(), "no channels yet") { t.Fatalf("no channels: %v", err) } } // --- Task 4: the reserved sidecar suffix, enforced at every minting door --- // // Task 3 already refuses to SERVE a Kind=="sidecar" row structurally // (edge.go's Reload skips it at the top of the loop). These rails are // defence in depth behind that guarantee, and they close the OTHER // direction: a row of a DIFFERENT kind minted (by hand, or before this // suffix was reserved) with a Host under provision.SidecarSuffix, which // would be served — undermining the very reservation Task 3 depends on. // TestAddRefusesSidecarSuffixAndKind pins addRail's predicate exactly as the // task-4 brief specifies it. func TestAddRefusesSidecarSuffixAndKind(t *testing.T) { if err := addRail("bot.eleven.bdm.sidecar.internal", "instance"); err == nil { t.Fatal("a host under the reserved suffix must be refused") } if err := addRail("app.example.com", "sidecar"); err == nil { t.Fatal("carlos add must not mint a sidecar row — sidecars are provisioned, not added") } if err := addRail("app.example.com", "instance"); err != nil { t.Fatalf("an ordinary add must still work: %v", err) } } func TestAddRefusesTheTmpNamespace(t *testing.T) { if err := appRail("tmp-docs"); err == nil { t.Fatal("carlos add must not route a tmp- app") } for _, app := range []string{"", "docs", "tmpdocs"} { if err := appRail(app); err != nil { t.Fatalf("appRail(%q) = %v, want nil", app, err) } } } // TestAddRefusesTheTmpNamespaceWiredIntoRunAdd proves runAdd calls appRail // before any registry write. func TestAddRefusesTheTmpNamespaceWiredIntoRunAdd(t *testing.T) { bin := buildCarlos(t) regPath := filepath.Join(t.TempDir(), "registry.db") cmd := exec.Command(bin, "add", "-registry", regPath, "-app", "tmp-docs", "-kind", "service", "-addr", "127.0.0.1:9", "svc.example.com") cmd.Env = carlosTestEnv(t) cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() if err == nil { t.Fatalf("carlos add --app tmp-docs: want non-zero exit, got 0\n%s", out) } if !strings.Contains(string(out), "tmp-") { t.Fatalf("refusal does not name the tmp- reservation:\n%s", out) } reg, rerr := registry.Open(regPath) if rerr != nil { t.Fatal(rerr) } defer reg.Close() if rt, gerr := reg.Get("svc.example.com"); gerr != nil || rt != nil { t.Fatalf("a refused add still wrote a row: %+v, %v", rt, gerr) } } // TestAddRefusesSidecarSuffixWiredIntoRunAdd drives `carlos add` as a // subprocess: addRail's own test proves the predicate, this proves runAdd // actually calls it, before any registry write. func TestAddRefusesSidecarSuffixWiredIntoRunAdd(t *testing.T) { bin := buildCarlos(t) tmp := t.TempDir() scratch := t.TempDir() regPath := filepath.Join(tmp, "registry.db") run := func(args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t) cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } host := "bot.eleven.bdm" + provision.SidecarSuffix out, err := run("add", "-registry", regPath, "-app", "eleven", host) if err == nil { t.Fatalf("carlos add %s: want non-zero exit, got 0\n%s", host, out) } if !strings.Contains(out, provision.SidecarSuffix) { t.Fatalf("refusal does not name the reserved suffix:\n%s", out) } reg, rerr := registry.Open(regPath) if rerr != nil { t.Fatalf("open registry: %v", rerr) } defer reg.Close() if rt, gerr := reg.Get(host); gerr != nil || rt != nil { t.Fatalf("a refused add still wrote a row: %+v, %v", rt, gerr) } out, err = run("add", "-registry", regPath, "-app", "eleven", "-kind", "sidecar", "app.example.com") if err == nil { t.Fatalf("carlos add -kind sidecar: want non-zero exit, got 0\n%s", out) } if !strings.Contains(out, "sidecar") { t.Fatalf("refusal does not mention sidecar:\n%s", out) } if rt, gerr := reg.Get("app.example.com"); gerr != nil || rt != nil { t.Fatalf("a refused -kind sidecar add still wrote a row: %+v, %v", rt, gerr) } } // TestSuffixRailRefusesReservedSuffix: `carlos route` cannot MINT a row // under the reserved suffix (r.Get returns nil and the verb refuses before // any rail runs) — but it CAN keep an existing one alive or grant it // capabilities, which is the door this rail closes. Pure and table-tested // for the same reason grantRail/backingRail are: a rail nobody can exercise // without running the whole process is a rail nobody tests. func TestSuffixRailRefusesReservedSuffix(t *testing.T) { under := registry.Route{Host: "bot.eleven.bdm" + provision.SidecarSuffix, Kind: "instance", App: "eleven", Account: "bdm"} err := suffixRail(under) if err == nil { t.Fatalf("suffixRail(%+v) accepted; want a refusal", under) } if !strings.Contains(err.Error(), provision.SidecarSuffix) { t.Fatalf("refusal %q does not name the reserved suffix", err) } ordinary := registry.Route{Host: "app.example.com", Kind: "instance", App: "eleven", Account: "bdm"} if err := suffixRail(ordinary); err != nil { t.Fatalf("suffixRail(%+v) = %v, want accepted", ordinary, err) } } // TestRouteRefusesReservedSuffixWiredIn drives `carlos route` as a // subprocess against a row planted directly in the registry — the only way // such a row can exist post-Task-4, since `carlos add` now refuses to mint // one — proving suffixRail is wired into every axis this verb writes, not // only provable in isolation. func TestRouteRefusesReservedSuffixWiredIn(t *testing.T) { bin := buildCarlos(t) tmp := t.TempDir() scratch := t.TempDir() regPath := filepath.Join(tmp, "registry.db") host := "bot.eleven.bdm" + provision.SidecarSuffix reg, err := registry.Open(regPath) if err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{ Host: host, Kind: "instance", App: "eleven", Account: "bdm", Status: "ready", }); err != nil { t.Fatal(err) } reg.Close() run := func(args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t) cmd.Dir = scratch out, err := cmd.CombinedOutput() return string(out), err } for _, args := range [][]string{ {"route", "-registry", regPath, "-host", host, "-channel", "canary/x"}, {"route", "-registry", regPath, "-host", host, "-grant", "port:25"}, {"route", "-registry", regPath, "-host", host, "-hibernate"}, {"route", "-registry", regPath, "-host", host, "-backing", registry.BackingUnit}, {"route", "-registry", regPath, "-host", host, "-unit", "console.service"}, } { out, err := run(args...) if err == nil { t.Fatalf("route %v: want non-zero exit, got 0\n%s", args[3:], out) } if !strings.Contains(out, provision.SidecarSuffix) { t.Fatalf("route %v: refusal does not name the reserved suffix:\n%s", args[3:], out) } } // The -app bulk sweep must not abort on a suffix row swept in alongside // ordinary ones (a real provisioned sidecar's App equals its owning // app's, so this is not hypothetical) — it is skipped, like a Platform // route, while its ordinary sibling still moves. reg2, err := registry.Open(regPath) if err != nil { t.Fatal(err) } if err := reg2.Upsert(registry.Route{ Host: "eleven.bdm.oncarlos.com", Kind: "instance", App: "eleven", Account: "bdm", Status: "ready", }); err != nil { t.Fatal(err) } reg2.Close() // -environment "-" (clear) is a no-op for a fresh route (Environment is // already "") and needs no bucket/pubkey setup — chosen so this exercises // the -app sweep's suffix skip without dragging in the channel axis's // store/signature machinery, which is not what this test is about. out, err := run("route", "-registry", regPath, "-app", "eleven", "-account", "bdm", "-environment", "-") if err != nil { t.Fatalf("route -app eleven -environment -: %v\n%s", err, out) } if !strings.Contains(out, "eleven.bdm.oncarlos.com") { t.Fatalf("the ordinary sibling route was not moved:\n%s", out) } // The suffix row is named in a SKIP line (like a Platform route's own // skip), not silently dropped — but it must never appear as WRITTEN. if !strings.Contains(out, host+": skipped") { t.Fatalf("the suffix row must be named in a skip line, not silently dropped:\n%s", out) } } // A nested row must stay activator-managed so the layout revert can stop // it: `carlos route --hibernate=false` refuses one, by path shape and // whatever else the row says. func TestRefuseUnflagNested(t *testing.T) { host := "acme.example.com" nested := registry.Route{Host: host, Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath("/data", host), Socket: provision.NestedSocket(host)} err := refuseUnflagNested(nested, "/data") want := "route: acme.example.com: its paths are nested. revert the layout first; a nested instance must stay managed so the revert can stop it" if err == nil || err.Error() != want { t.Fatalf("refuseUnflagNested(nested) = %v, want %q", err, want) } flat := nested flat.DBPath, flat.Socket = provision.DefaultDBPath("/data", host), provision.DefaultSocket(host) if err := refuseUnflagNested(flat, "/data"); err != nil { t.Fatalf("refuseUnflagNested(flat) = %v, want nil", err) } } // writeLayoutJournal puts a journal for host under dataDir/layout. The // guards only ask whether one is there, so its contents do not matter. func writeLayoutJournal(t *testing.T, dataDir, host string) { t.Helper() path := provision.LayoutJournalPath(dataDir, host) if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte("{}"), 0o600); err != nil { t.Fatal(err) } } // A row whose paths are still flat can be mid-move: the journal is what // says so. Unflagging it would take it out of the management recovery // needs, so the guard refuses it with the same approved text. func TestRefuseUnflagNestedRefusesAFlatRowWithAJournal(t *testing.T) { host := "acme.example.com" dd := t.TempDir() flat := registry.Route{Host: host, Kind: "instance", Hibernate: true, DBPath: provision.DefaultDBPath(dd, host), Socket: provision.DefaultSocket(host)} writeLayoutJournal(t, dd, host) err := refuseUnflagNested(flat, dd) want := "route: acme.example.com: its paths are nested. revert the layout first; a nested instance must stay managed so the revert can stop it" if err == nil || err.Error() != want { t.Fatalf("refuseUnflagNested(flat, journal) = %v, want %q", err, want) } } // A re-add over a host with a journal would rewrite the row a recovery is // about to check and flip, row or no row, so it refuses too. func TestRefuseAddNestedRefusesAHostWithAJournal(t *testing.T) { host := "acme.example.com" dd := t.TempDir() flat := ®istry.Route{Host: host, Kind: "instance", DBPath: provision.DefaultDBPath(dd, host), Socket: provision.DefaultSocket(host)} writeLayoutJournal(t, dd, host) want := "add acme.example.com: its paths are nested. revert the layout first; re-adding it would point it back at the shared directory" for name, existing := range map[string]*registry.Route{"flat row": flat, "no row": nil} { if err := refuseAddNested(host, existing, dd); err == nil || err.Error() != want { t.Errorf("%s: refuseAddNested = %v, want %q", name, err, want) } } // A journal this process cannot even read the target of still counts. other := "other.example.com" if err := os.Symlink("/nonexistent", provision.LayoutJournalPath(dd, other)); err != nil { t.Fatal(err) } if err := refuseAddNested(other, nil, dd); err == nil { t.Error("a dangling journal symlink was not taken as a journal") } } // `carlos add` over an existing nested host would rewrite its paths to the // flat defaults and strand the files under the instance tree, so it // refuses. A new host, and a re-add of a flat one, go ahead. func TestRefuseAddNested(t *testing.T) { host := "acme.example.com" nested := ®istry.Route{Host: host, Kind: "instance", Hibernate: true, DBPath: provision.NestedDBPath("/data", host), Socket: provision.NestedSocket(host)} err := refuseAddNested(host, nested, "/data") want := "add acme.example.com: its paths are nested. revert the layout first; re-adding it would point it back at the shared directory" if err == nil || err.Error() != want { t.Fatalf("refuseAddNested(nested) = %v, want %q", err, want) } if err := refuseAddNested(host, nil, "/data"); err != nil { t.Fatalf("refuseAddNested(new host) = %v, want nil", err) } flat := ®istry.Route{Host: host, Kind: "instance", DBPath: provision.DefaultDBPath("/data", host), Socket: provision.DefaultSocket(host)} if err := refuseAddNested(host, flat, "/data"); err != nil { t.Fatalf("refuseAddNested(flat) = %v, want nil", err) } } // `carlos add` mints the nested defaults only when the box's layout // intent names nested; no store means flat as before, and an unreadable // intent is a refusal, not a guess. func TestAddMintsNestedOnlyUnderANestedIntent(t *testing.T) { ctx := context.Background() label := func() string { return "kass-1" } if nested, err := addMintsNested(ctx, nil, label, nil, "/data"); err != nil || nested { t.Fatalf("no store = %v %v, want flat", nested, err) } s := release.NewDirStore(t.TempDir()) if nested, err := addMintsNested(ctx, s, label, nil, "/data"); err != nil || nested { t.Fatalf("no intent = %v %v, want flat", nested, err) } if _, err := release.AppendLayoutIntent(ctx, s, release.LayoutIntent{Mode: release.LayoutModeNested, Scope: release.LayoutScope{Box: "kass-2"}}); err != nil { t.Fatal(err) } if nested, err := addMintsNested(ctx, s, label, nil, "/data"); err != nil || nested { t.Fatalf("another box's intent = %v %v, want flat", nested, err) } if _, err := release.AppendLayoutIntent(ctx, s, release.LayoutIntent{Mode: release.LayoutModeNested, Scope: release.LayoutScope{Box: "kass-1"}}); err != nil { t.Fatal(err) } if nested, err := addMintsNested(ctx, s, label, nil, "/data"); err != nil || !nested { t.Fatalf("this box's nested intent = %v %v, want nested", nested, err) } if _, err := release.AppendLayoutIntent(ctx, s, release.LayoutIntent{Mode: release.LayoutModeFlat}); err != nil { t.Fatal(err) } if nested, err := addMintsNested(ctx, s, label, nil, "/data"); err != nil || nested { t.Fatalf("a later fleet-wide flat intent = %v %v, want flat", nested, err) } // Unreadable (a newer schema): refused with the retry named. if err := s.Delete(ctx, release.LayoutIntentKey()); err != nil { t.Fatal(err) } if _, err := s.Put(ctx, release.LayoutIntentKey(), []byte(`{"schema":2,"requests":[]}`), ""); err != nil { t.Fatal(err) } if _, err := addMintsNested(ctx, s, label, nil, "/data"); err == nil || !strings.Contains(err.Error(), "pass --socket and --db") { t.Fatalf("unreadable intent = %v, want a refusal naming the flags", err) } } // With no intent recorded, `carlos add` mints nested when the box's own // registry already holds an activator-managed row at the nested paths // (the box is nested by default and has moved); a recorded flat intent // wins over that evidence; an unmanaged or external nested-pathed row is // not evidence. func TestAddMintsNestedFromTheBoxsOwnRowsWhenNoIntentIsRecorded(t *testing.T) { ctx := context.Background() label := func() string { return "kass-1" } host := "a.example.com" managedNested := registry.Route{Host: host, Kind: "instance", Hibernate: true, Socket: provision.NestedSocket(host), DBPath: provision.NestedDBPath("/data", host)} unflaggedNested := registry.Route{Host: host, Kind: "instance", Socket: provision.NestedSocket(host), DBPath: provision.NestedDBPath("/data", host)} flat := registry.Route{Host: host, Kind: "instance", Hibernate: true, Socket: provision.DefaultSocket(host), DBPath: provision.DefaultDBPath("/data", host)} if nested, err := addMintsNested(ctx, nil, label, []registry.Route{flat}, "/data"); err != nil || nested { t.Fatalf("flat rows only = %v %v, want flat", nested, err) } if nested, err := addMintsNested(ctx, nil, label, []registry.Route{unflaggedNested}, "/data"); err != nil || nested { t.Fatalf("an unmanaged nested-pathed row = %v %v, want flat (not evidence)", nested, err) } if nested, err := addMintsNested(ctx, nil, label, []registry.Route{flat, managedNested}, "/data"); err != nil || !nested { t.Fatalf("a managed nested row = %v %v, want nested", nested, err) } s := release.NewDirStore(t.TempDir()) if nested, err := addMintsNested(ctx, s, label, []registry.Route{managedNested}, "/data"); err != nil || !nested { t.Fatalf("store with no intent, nested evidence = %v %v, want nested", nested, err) } if _, err := release.AppendLayoutIntent(ctx, s, release.LayoutIntent{Mode: release.LayoutModeFlat}); err != nil { t.Fatal(err) } if nested, err := addMintsNested(ctx, s, label, []registry.Route{managedNested}, "/data"); err != nil || nested { t.Fatalf("a recorded flat intent must win over the evidence: %v %v", nested, err) } }