// Black-box tests for the TypeScript bundle coverage-summary script. // // The script ships into a project's .claude/scripts/ and runs via `node`, so // these tests exercise the real CLI rather than importing it: build a temp tree // (source files + an istanbul json-summary report), run the script against it, // and assert on stdout. Run with `node --test`. // // Normal / Boundary / Error coverage at the behavior level: missing-file // detection, the file-weighted number, all-tracked, ignoring test files and // node_modules, and a missing-report error. const { test } = require("node:test"); const assert = require("node:assert"); const { execFileSync } = require("node:child_process"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const SCRIPT = path.resolve(__dirname, "..", "claude", "scripts", "coverage-summary.js"); // fixture builds a temp project. sources maps relpath -> contents. report is the // istanbul json-summary object, with per-file keys given as relpaths that get // rewritten to absolute paths (matching how istanbul records them). function fixture(sources, reportRel) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "tscov-")); for (const [rel, body] of Object.entries(sources)) { const full = path.join(root, rel); fs.mkdirSync(path.dirname(full), { recursive: true }); fs.writeFileSync(full, body); } const report = { total: {} }; for (const [rel, stmts] of Object.entries(reportRel)) { report[path.join(root, rel)] = { statements: { total: stmts.total, covered: stmts.covered, pct: 0 }, lines: { total: stmts.total, covered: stmts.covered, pct: 0 }, }; } const reportPath = path.join(root, "coverage-summary.json"); fs.writeFileSync(reportPath, JSON.stringify(report)); return { root, reportPath }; } function run(reportPath, sourceDir, root) { return execFileSync("node", [SCRIPT, reportPath, sourceDir, root], { encoding: "utf8" }); } test("missing file is surfaced and counted 0%", () => { const { root, reportPath } = fixture( { "src/a.ts": "export const a = 1;\n", "src/untested.ts": "export const u = 2;\n" }, { "src/a.ts": { total: 2, covered: 2 } }, // a.ts 100%; untested.ts absent ); const out = run(reportPath, path.join(root, "src"), root); assert.match(out, /src\/untested\.ts/); assert.match(out, /0%/); assert.match(out, /50\.0%/); // a=100, untested missing=0 -> 50 }); test("all tracked, nothing missing", () => { const { root, reportPath } = fixture( { "src/a.ts": "export const a = 1;\n" }, { "src/a.ts": { total: 2, covered: 2 } }, ); const out = run(reportPath, path.join(root, "src"), root); assert.match(out, /Not in coverage report: 0 file/); assert.match(out, /100\.0%/); }); test("test files and node_modules are not counted as source", () => { const { root, reportPath } = fixture( { "src/a.ts": "export const a = 1;\n", "src/a.test.ts": "import {a} from './a';\n", "node_modules/dep/index.js": "module.exports = 1;\n", }, { "src/a.ts": { total: 2, covered: 2 } }, ); const out = run(reportPath, path.join(root, "src"), root); assert.doesNotMatch(out, /a\.test\.ts/); assert.doesNotMatch(out, /node_modules/); assert.match(out, /100\.0%/); }); test("missing report exits non-zero", () => { const { root } = fixture({ "src/a.ts": "export const a = 1;\n" }, {}); assert.throws(() => execFileSync("node", [SCRIPT, path.join(root, "nope.json"), path.join(root, "src"), root], { encoding: "utf8", stdio: "pipe", }), ); });