import { spawnSync } from "node:child_process"; import { describe, expect, test } from "bun:test"; import { repoRoot } from "./config"; describe("CLI input error hierarchy", () => { test("unknown root commands are compact and stack-free by default", () => { const result = runCli("definitely-not-a-command"); expect(result.status).toBe(1); expect(result.stderr).toBe(""); expect(result.stdout.trim().split("\n").length).toBeLessThanOrEqual(8); expect(result.stdout).toContain("ERROR cli-input/unknown-command"); expect(result.stdout).toContain("usage: bun scripts/cli.ts help"); expect(result.stdout).not.toContain("stack"); expect(result.stdout).not.toContain(" at "); }); test("explicit machine output returns a JSON input-error envelope without a stack", () => { for (const args of [ ["definitely-not-a-command", "--json"], ["definitely-not-a-command", "--full"], ["definitely-not-a-command", "--raw"], ["definitely-not-a-command", "-o", "json"], ]) { const result = runCli(...args); expect(result.status).toBe(1); expect(result.stderr).toBe(""); const payload = JSON.parse(result.stdout) as { ok: boolean; error: { kind: string; code: string; debug: boolean; stack?: string }; }; expect(payload.ok).toBe(false); expect(payload.error.kind).toBe("cli-input"); expect(payload.error.code).toBe("unknown-command"); expect(payload.error.debug).toBe(false); expect(payload.error.stack).toBeUndefined(); expect(result.stdout).not.toContain("stackOmitted"); } const nonJsonOutput = runCli("definitely-not-a-command", "-o", "yaml"); expect(nonJsonOutput.status).toBe(1); expect(nonJsonOutput.stdout).toContain("ERROR cli-input/unknown-command"); expect(() => JSON.parse(nonJsonOutput.stdout)).toThrow(); }); test("predictable Gitea option validation is compact and stack-free", () => { const result = runCli("platform-infra", "gitea", "status", "--target", "NC01", "--definitely-unsupported"); expect(result.status).toBe(1); expect(result.stderr).toBe(""); expect(result.stdout.trim().split("\n").length).toBeLessThanOrEqual(8); expect(result.stdout).toContain("ERROR cli-input/unsupported-option"); expect(result.stdout).toContain("argument: --definitely-unsupported"); expect(result.stdout).not.toContain("stack"); expect(result.stdout).not.toContain("platform-infra-gitea.ts:"); }); test("Gitea full and raw input errors preserve machine JSON without a stack", () => { for (const outputFlag of ["--full", "--raw"]) { const result = runCli("platform-infra", "gitea", "status", "--target", "NC01", outputFlag, "--definitely-unsupported"); expect(result.status).toBe(1); expect(result.stderr).toBe(""); const payload = JSON.parse(result.stdout) as { ok: boolean; error: { kind: string; code: string; argument: string; debug: boolean; stack?: string }; }; expect(payload.ok).toBe(false); expect(payload.error.kind).toBe("cli-input"); expect(payload.error.code).toBe("unsupported-option"); expect(payload.error.argument).toBe("--definitely-unsupported"); expect(payload.error.debug).toBe(false); expect(payload.error.stack).toBeUndefined(); } }); test("only an explicitly unknown command target is converted to a compact input error", () => { const unknownTarget = runCli("platform-infra", "gitea", "status", "--target", "definitely-nope"); expect(unknownTarget.status).toBe(1); expect(unknownTarget.stderr).toBe(""); expect(unknownTarget.stdout.trim().split("\n").length).toBeLessThanOrEqual(8); expect(unknownTarget.stdout).toContain("ERROR cli-input/unknown-target"); expect(unknownTarget.stdout).toContain("argument: definitely-nope"); expect(unknownTarget.stdout).not.toContain("stack"); const internalConfig = spawnSync("bun", ["-e", [ "import { emitError } from './scripts/src/output.ts';", "import { resolveTarget } from './scripts/src/platform-infra-gitea-config.ts';", "try { resolveTarget({ defaults: { targetId: 'NC01' }, targets: [] } as any, 'NC01'); }", "catch (error) { emitError('internal-gitea-config-probe', error); }", ].join(" ")], { cwd: repoRoot, env: process.env, encoding: "utf8", }); expect(internalConfig.status).toBe(0); const internalPayload = JSON.parse(internalConfig.stdout) as { error: { message: string; stack: string } }; expect(internalPayload.error.message).toContain("unknown gitea target NC01"); expect(internalPayload.error.stack).toContain("platform-infra-gitea-config.ts"); }); test("explicit debug retains stack while internal exceptions remain observable", () => { const debug = runCliWithEnv({ UNIDESK_CLI_DEBUG: "1" }, "definitely-not-a-command"); expect(debug.status).toBe(1); const debugPayload = JSON.parse(debug.stdout) as { error: { debug: boolean; stack: string } }; expect(debugPayload.error.debug).toBe(true); expect(debugPayload.error.stack).toContain("CliInputError: Unknown command"); const internal = spawnSync("bun", ["-e", "import { emitError } from './scripts/src/output.ts'; emitError('internal-probe', new Error('internal-probe-failed'));"], { cwd: repoRoot, env: process.env, encoding: "utf8", }); expect(internal.status).toBe(0); const internalPayload = JSON.parse(internal.stdout) as { error: { message: string; stack: string } }; expect(internalPayload.error.message).toBe("internal-probe-failed"); expect(internalPayload.error.stack).toContain("Error: internal-probe-failed"); }); }); function runCli(...args: string[]) { return runCliWithEnv({}, ...args); } function runCliWithEnv(env: NodeJS.ProcessEnv, ...args: string[]) { return spawnSync("bun", ["scripts/cli.ts", ...args], { cwd: repoRoot, env: { ...process.env, ...env }, encoding: "utf8", }); }