diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 13:57:40 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 13:57:40 -0500 |
| commit | ee903e4b63257573773e93d10612250e3634cae9 (patch) | |
| tree | 89cc6199b53f445a4c32252c6b982a6c86964e3b /languages/typescript/tests | |
| parent | 47ca509e69b6a1472a735a4b9521a952e7434491 (diff) | |
| download | rulesets-ee903e4b63257573773e93d10612250e3634cae9.tar.gz rulesets-ee903e4b63257573773e93d10612250e3634cae9.zip | |
feat(typescript): add coverage-summary to the TypeScript bundle
Last language in the coverage-summary fan-out, after Elisp, Python, and Go. Same kernel: count every source file on disk that's absent from the coverage report as 0% and weight the project number by file, so an untested file stays visible instead of being averaged away.
The script at languages/typescript/claude/scripts/coverage-summary.js parses an Istanbul json-summary report (the coverage-summary.json that c8, Vitest, and Jest all emit), takes per-file statements covered over total, and reports a file-weighted number plus the missing files. It walks the source dir for .ts/.js, skipping test files, declarations, and node_modules. Node built-ins only, so it runs via node with no install, and it doesn't reimplement the per-file table nyc already prints.
Tests are black-box, run with node's own test runner: a temp tree plus a json-summary report, the script invoked via node, output asserted. They cover missing-file detection, all-tracked, test-file and node_modules exclusion, and the missing-report error. make test gained a node --test discovery path for languages/*/tests, guarded so environments without Node skip it cleanly. As with Python, the TypeScript bundle had no gitignore-add.txt, which would have left the script un-gitignored on install, so I added one.
This finishes the fan-out: coverage-summary now ships in all four bundles, each parsing its own tool's report behind the same file-weighted, missing-as-0% kernel. I proved the Go and TypeScript scripts by running them (Go against a live profile, TS against a synthetic report and the CLI). Python and TypeScript weren't run against a live coverage tool, since neither coverage.py nor nyc is installed here, so the first adopter of each should check against a real report.
Diffstat (limited to 'languages/typescript/tests')
| -rw-r--r-- | languages/typescript/tests/coverage-summary.test.js | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/languages/typescript/tests/coverage-summary.test.js b/languages/typescript/tests/coverage-summary.test.js new file mode 100644 index 0000000..b2b6ea3 --- /dev/null +++ b/languages/typescript/tests/coverage-summary.test.js @@ -0,0 +1,90 @@ +// 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", + }), + ); +}); |
