import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { describe, test } from "node:test"; const script = fileURLToPath(new URL("./validate-pi.mjs", import.meta.url)); const installedPackage = process.env.PI_CONTEXT_PACKAGE_JSON_PATH; const examples = { A: 'flowchart TD\n A["Agent"] --> B["Review"]', B: 'sequenceDiagram\n participant H as You\n participant A as Agent\n H->>A: Task\n A-->>H: Patch', C: 'flowchart LR\n A["Prepare environment"] --> B["Run agent assignment"] --> C["Collect test evidence"] --> D["Review proposed changes"] --> E["Integrate accepted revision"]', D: 'gantt\n title Agent work\n dateFormat YYYY-MM-DD\n section Work\n Review :2026-01-01, 1d', E: 'flowchart TD\n A[Start --> B', }; function invoke(source = examples.A, args = ["--width", "48"], options = {}) { const result = spawnSync(process.execPath, [script, ...args], { input: source, encoding: "utf8", timeout: 10_000, ...options, }); assert.ifError(result.error); assert.equal(result.signal, null); return result; } function withoutPi() { const env = { ...process.env }; delete env.PI_CONTEXT_PACKAGE_JSON_PATH; return env; } test("help requires neither Pi nor source", () => { const result = invoke("", ["--help"], { env: withoutPi() }); assert.equal(result.status, 0); assert.match(result.stdout, /--width/); }); test("requires explicit positive integer width; rejects file arguments", () => { for (const args of [[], ["--width", "0"], ["--width", "-1"], ["--width", "1.5"], ["--width", "abc"], ["--width", "9007199254740992"], ["--width", "48", "diagram.mmd"]]) { const result = invoke("", args, { env: withoutPi() }); assert.equal(result.status, 2); assert.match(result.stderr, /Usage:/); } }); test("missing Pi context fails explicitly without fallback", () => { const result = invoke(examples.A, undefined, { env: withoutPi() }); assert.equal(result.status, 2); assert.match(result.stderr, /PI_CONTEXT_PACKAGE_JSON_PATH/); }); test("relative Pi package path is rejected", () => { const result = invoke(examples.A, undefined, { env: { ...withoutPi(), PI_CONTEXT_PACKAGE_JSON_PATH: "package.json" } }); assert.equal(result.status, 2); assert.match(result.stderr, /absolute path/); }); test("missing Pi renderer fails without installing dependencies", () => { const root = mkdtempSync(join(tmpdir(), "mermaid-pi-missing-")); try { const manifest = join(root, "package.json"); writeFileSync(manifest, "{}"); const result = invoke(examples.A, undefined, { cwd: root, env: { ...withoutPi(), PI_CONTEXT_PACKAGE_JSON_PATH: manifest } }); assert.equal(result.status, 2); assert.match(result.stderr, /Cannot load Pi's installed grok-mermaid/); assert.deepEqual(readdirSync(root), ["package.json"]); } finally { rmSync(root, { recursive: true, force: true }); } }); describe("installed Pi renderer", { skip: !installedPackage && "PI_CONTEXT_PACKAGE_JSON_PATH is needed for renderer integration tests" }, () => { for (const name of ["A", "B"]) { test(`${name}: narrow diagram passes with preview`, () => { const result = invoke(examples[name]); assert.equal(result.status, 0, result.stderr); const report = JSON.parse(result.stdout); assert.equal(report.ok, true); assert.deepEqual(report.warnings, []); assert.equal(report.overflow, false); assert.ok(report.lines.length > 0); }); } test("C: wide diagram fails width budget", () => { const result = invoke(examples.C); assert.equal(result.status, 1, result.stderr); const report = JSON.parse(result.stdout); assert.equal(report.ok, false); assert.equal(report.overflow, true); assert.ok(report.requiredColumns > 48); }); test("D: unsupported diagram fails", () => { const result = invoke(examples.D); assert.equal(result.status, 1, result.stderr); assert.equal(JSON.parse(result.stdout).requiredColumns, null); }); test("E: warnings fail even when the partial drawing fits", () => { const result = invoke(examples.E); assert.equal(result.status, 1, result.stderr); const report = JSON.parse(result.stdout); assert.equal(report.ok, false); assert.equal(report.overflow, false); assert.ok(report.warnings.length > 0); }); test("empty stdin fails", () => { const result = invoke(""); assert.equal(result.status, 1, result.stderr); assert.equal(JSON.parse(result.stdout).ok, false); }); test("exact width fits; one column less fails", () => { const width = JSON.parse(invoke().stdout).requiredColumns; assert.equal(invoke(examples.A, ["--width", String(width)]).status, 0); assert.equal(invoke(examples.A, ["--width", String(width - 1)]).status, 1); }); test("stdin validation from a Unicode/space cwd creates no files", () => { const root = mkdtempSync(join(tmpdir(), "mermaid pi #é-")); try { const result = invoke(examples.A, undefined, { cwd: root }); assert.equal(result.status, 0, result.stderr); assert.deepEqual(readdirSync(root), []); } finally { rmSync(root, { recursive: true, force: true }); } }); });