aboutsummaryrefslogtreecommitdiff
path: root/languages/typescript/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-31 13:57:40 -0500
committerCraig Jennings <c@cjennings.net>2026-05-31 13:57:40 -0500
commitee903e4b63257573773e93d10612250e3634cae9 (patch)
tree89cc6199b53f445a4c32252c6b982a6c86964e3b /languages/typescript/tests
parent47ca509e69b6a1472a735a4b9521a952e7434491 (diff)
downloadrulesets-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.js90
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",
+ }),
+ );
+});