package main // The CLI-side gates for `carlos releases retention` (issue #136): these // fail before any network call, so they run against the real binary rather // than a fake console. import ( "os/exec" "strings" "testing" ) // runRetentionCLI shells the built binary with the given args from a // scratch dir with the masked test env (the 40-accounts rule). Deviation // from the brief: there is no `carlosBin(t)` helper in this package: the // existing per-test build helper is `buildCarlos(t)` (auth_e2e_test.go), // which restart_test.go's own CLI-gate test already uses — followed // verbatim here rather than inventing a second name for the same thing. func runRetentionCLI(t *testing.T, args ...string) (string, error) { t.Helper() bin := buildCarlos(t) cmd := exec.Command(bin, args...) cmd.Env = carlosTestEnv(t) cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() return string(out), err } func TestReleasesRetentionRefusesBelowFloorClientSide(t *testing.T) { out, err := runRetentionCLI(t, "releases", "retention", "-app", "kass", "-keep", "5") if err == nil { t.Fatalf("keep 5 exited 0: %s", out) } if !strings.Contains(out, "20") { t.Fatalf("floor refusal must name the floor, got: %s", out) } } func TestReleasesRetentionRefusesOffPlusKeep(t *testing.T) { out, err := runRetentionCLI(t, "releases", "retention", "-app", "kass", "-keep", "50", "-off") if err == nil { t.Fatalf("--off with --keep exited 0: %s", out) } if !strings.Contains(out, "-off") { t.Fatalf("refusal should name the conflict, got: %s", out) } } func TestReleasesRetentionNeedsApp(t *testing.T) { // -app resolves since the project-defaults spec (2026-08-13): with // nothing configured the refusal names the missing SESSION, the real // blocker — not a usage line for a flag that is no longer required. out, err := runRetentionCLI(t, "releases", "retention") if err == nil { t.Fatalf("no --app exited 0: %s", out) } if !strings.Contains(out, "no deployment target configured") { t.Fatalf("want the no-target refusal, got: %s", out) } }