import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, expect, it } from "@suaveplan/testing/runner"; import { parseCliArgs, runCli } from "./cli.js"; const HERE = dirname(fileURLToPath(import.meta.url)); const FIXTURE = resolve(HERE, "../../tests/fixtures/tiny-plugin/src/index.ts"); describe("parseCliArgs", () => { it("parses entry + outDir", { timeout: 5000 }, () => { const a = parseCliArgs(["./entry.ts", "--out-dir", "/tmp/out"]); expect(a.entry).toBe("./entry.ts"); expect(a.outDir).toBe("/tmp/out"); expect(a.optimizeFor).toBe("size"); expect(a.sourcemaps).toBe(false); expect(a.help).toBe(false); }); it("parses --optimize-for and --sourcemaps", { timeout: 5000 }, () => { const a = parseCliArgs([ "./entry.ts", "--out-dir", "/tmp/out", "--optimize-for", "speed", "--sourcemaps", ]); expect(a.optimizeFor).toBe("speed"); expect(a.sourcemaps).toBe(true); }); it("raises on invalid optimize-for", { timeout: 5000 }, () => { expect(() => parseCliArgs(["x.ts", "--out-dir", "out", "--optimize-for", "tiny"]) ).toThrow(/optimize-for/); }); it("raises on missing entry", { timeout: 5000 }, () => { expect(() => parseCliArgs(["--out-dir", "out"])).toThrow(/entry/); }); it("raises on missing out-dir", { timeout: 5000 }, () => { expect(() => parseCliArgs(["entry.ts"])).toThrow(/out-dir/); }); it("raises on unknown flag and extra positional", { timeout: 5000 }, () => { expect(() => parseCliArgs(["x.ts", "--out-dir", "o", "--bogus"]) ).toThrow(/unknown flag/); expect(() => parseCliArgs(["x.ts", "y.ts", "--out-dir", "o"])).toThrow( /positional/ ); }); it("raises when --out-dir is at end without value", { timeout: 5000, }, () => { expect(() => parseCliArgs(["x.ts", "--out-dir"])).toThrow( /requires a value/ ); }); it("honours -h and --help", { timeout: 5000 }, () => { expect(parseCliArgs(["-h"]).help).toBe(true); expect(parseCliArgs(["--help"]).help).toBe(true); }); }); describe("runCli", () => { it("compiles a fixture and prints a one-line JSON result", { timeout: 120000, }, async () => { const out = mkdtempSync(join(tmpdir(), "cli-ok-")); const stdoutLines: string[] = []; const stderrLines: string[] = []; let exitCode = -1; await runCli([FIXTURE, "--out-dir", out], { stdout: (l) => stdoutLines.push(l), stderr: (l) => stderrLines.push(l), exit: (c) => { exitCode = c; }, }); expect(exitCode).toBe(0); const parsed = JSON.parse(stdoutLines[0] ?? "{}"); expect(parsed.wasmPath.endsWith(".wasm")).toBe(true); expect(parsed.metadataPath.endsWith(".wasm.json")).toBe(true); expect(parsed.routes).toBeGreaterThan(0); rmSync(out, { recursive: true, force: true }); }); it("emits structured error JSON on bad args (exit 2)", { timeout: 5000, }, async () => { const stderrLines: string[] = []; let exitCode = -1; await runCli([], { stdout: () => undefined, stderr: (l) => stderrLines.push(l), exit: (c) => { exitCode = c; }, }); expect(exitCode).toBe(2); const env = JSON.parse(stderrLines[0] ?? "{}"); expect(env.error).toBe("INVALID_ARGS"); }); it("exits 0 on --help", { timeout: 5000 }, async () => { const stdoutLines: string[] = []; let exitCode = -1; await runCli(["--help"], { stdout: (l) => stdoutLines.push(l), stderr: () => undefined, exit: (c) => { exitCode = c; }, }); expect(exitCode).toBe(0); expect( stdoutLines[0]?.includes("Usage") || stdoutLines[0]?.includes("suaveplan-wasm-compile") ).toBe(true); }); it("exits 1 on a compile failure (missing entry file)", { timeout: 30000, }, async () => { const stderrLines: string[] = []; let exitCode = -1; await runCli(["/no/such/file.ts", "--out-dir", "/tmp/x"], { stdout: () => undefined, stderr: (l) => stderrLines.push(l), exit: (c) => { exitCode = c; }, }); expect(exitCode).toBe(1); const env = JSON.parse(stderrLines[0] ?? "{}"); expect(typeof env.code).toBe("string"); }); it("wraps non-WasmCompilerError as WASM_COMPILER.UNCAUGHT", { timeout: 5000, }, async () => { const stderrLines: string[] = []; let exitCode = -1; await runCli([FIXTURE, "--out-dir", "/tmp/cli-uncaught"], { stdout: () => undefined, stderr: (l) => stderrLines.push(l), exit: (c) => { exitCode = c; }, compile: async () => { throw new Error("synthetic non-wasm-compiler error"); }, }); expect(exitCode).toBe(1); const env = JSON.parse(stderrLines[0] ?? "{}"); expect(env.code).toBe("WASM_COMPILER.UNCAUGHT"); }); });