/* Unit tests for run tool covering runTool. It also guards the documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-1311: Compass header sweep — declares the run suite's contract for test-file scaffold coverage. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { runTool } from "../run-tool.ts"; import type { ToolSpec, ToolResult, ToolExecutor } from "../run-tool.ts"; function recordingExecutor( calls: ToolSpec[], result: ToolResult = { success: true, stdout: "ok" }, ): ToolExecutor { return (spec) => { calls.push(spec); return result; }; } describe("runTool", () => { let cwd: string; beforeEach(async () => { cwd = await mkdtemp(join(tmpdir(), "run-tool-")); }); afterEach(async () => { await rm(cwd, { recursive: true, force: true }); }); it("passes the spec to the executor and returns its result", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["vite", "build"], cwd, timeoutMs: 5_000 }, recordingExecutor(calls), ); expect(result.success).toBe(true); expect(result.stdout).toBe("ok"); expect(calls).toHaveLength(1); expect(calls[0]!.bin).toBe("npx"); expect(calls[0]!.args).toEqual(["vite", "build"]); expect(calls[0]!.cwd).toBe(cwd); expect(calls[0]!.timeoutMs).toBe(5_000); }); it("fails preflight when requireDist and dist/ is missing", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["deploy"], cwd, requireDist: true }, recordingExecutor(calls), ); expect(result.success).toBe(false); expect(result.failedAt).toBe("preflight"); expect(result.errors?.[0]).toContain("dist/ directory not found"); expect(calls).toHaveLength(0); }); it("passes preflight when requireDist and dist/ exists", async () => { await mkdir(join(cwd, "dist"), { recursive: true }); const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["deploy"], cwd, requireDist: true }, recordingExecutor(calls), ); expect(result.success).toBe(true); expect(calls).toHaveLength(1); }); it("fails preflight when a required env var is missing", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["x"], cwd, requireEnv: ["DEFINITELY_MISSING_ENV_VAR"] }, recordingExecutor(calls), ); expect(result.success).toBe(false); expect(result.failedAt).toBe("preflight"); expect(result.errors?.[0]).toContain("DEFINITELY_MISSING_ENV_VAR"); expect(calls).toHaveLength(0); }); it("passes preflight when a required env var comes from spec.env", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["x"], cwd, env: { MY_TOKEN: "secret" }, requireEnv: ["MY_TOKEN"], }, recordingExecutor(calls), ); expect(result.success).toBe(true); expect(calls[0]!.env?.MY_TOKEN).toBe("secret"); }); it("fails preflight when a required env var is empty after merge", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["x"], cwd, env: { EMPTY_TOKEN: "" }, requireEnv: ["EMPTY_TOKEN"], }, recordingExecutor(calls), ); expect(result.success).toBe(false); expect(result.failedAt).toBe("preflight"); }); it("maps an executor throw to an exec failure", async () => { const throwing: ToolExecutor = () => { throw new Error("spawn npx ENOENT"); }; const result = await runTool({ bin: "npx", args: ["x"], cwd }, throwing); expect(result.success).toBe(false); expect(result.failedAt).toBe("exec"); expect(result.errors?.[0]).toContain("spawn npx ENOENT"); }); it("propagates executor failure results unchanged", async () => { const calls: ToolSpec[] = []; const result = await runTool( { bin: "npx", args: ["x"], cwd }, recordingExecutor(calls, { success: false, errors: ["exit 1"], failedAt: "exec" }), ); expect(result.success).toBe(false); expect(result.errors).toEqual(["exit 1"]); }); });