import { test, describe } from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { parseLcov, parseGitDiff, computeDiffCoverage } from "./diff.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURES = path.join(__dirname, "__fixtures__"); const lcovText = fs.readFileSync(path.join(FIXTURES, "sample.lcov"), "utf8"); const diffText = fs.readFileSync( path.join(FIXTURES, "sample-diff.patch"), "utf8", ); describe("parseLcov", () => { test("groups DA records by SF file", () => { const lcov = parseLcov(lcovText); assert.equal(lcov.size, 3); assert.ok( lcov.has( "/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts", ), ); assert.ok(lcov.has("/repo/packages/auth/src/entities/models/user.ts")); }); test("preserves per-line execution counts", () => { const lcov = parseLcov(lcovText); const lines = lcov.get( "/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts", ); assert.equal(lines.get(1), 5); assert.equal(lines.get(5), 0); assert.equal(lines.get(8), 3); }); test("ignores non-DA records (LF, LH, BRDA)", () => { const lcov = parseLcov(lcovText); const lines = lcov.get("/repo/packages/auth/src/entities/models/user.ts"); assert.equal(lines.size, 3); // Only DA records, not LF/LH counters }); }); describe("parseGitDiff", () => { test("extracts new + modified line numbers per file from the new version", () => { const diff = parseGitDiff(diffText); const signIn = diff.get( "packages/auth/src/application/use-cases/sign-in.use-case.ts", ); // Hunks: +2,2 (lines 2,3) and +5,2 (lines 5,6) assert.deepEqual( [...signIn].sort((a, b) => a - b), [2, 3, 5, 6], ); }); test("strips the b/ prefix from new-version paths", () => { const diff = parseGitDiff(diffText); assert.ok(diff.has("packages/auth/src/entities/models/user.ts")); assert.ok(!diff.has("b/packages/auth/src/entities/models/user.ts")); }); test("handles single-line hunks (no comma in +N,M)", () => { const diff = parseGitDiff(diffText); const blog = diff.get( "packages/blog/src/application/use-cases/get-article.use-case.ts", ); // @@ -11,0 +12 @@ -> single line at 12 assert.deepEqual([...blog], [12]); }); test("handles new files (entire file is in the diff)", () => { const diff = parseGitDiff(diffText); const upload = diff.get( "packages/media/src/application/use-cases/upload.use-case.ts", ); // @@ -0,0 +1,5 @@ -> lines 1-5 assert.deepEqual( [...upload].sort((a, b) => a - b), [1, 2, 3, 4, 5], ); }); }); describe("computeDiffCoverage", () => { test("passes when changed executable lines are all covered", () => { const lcov = parseLcov(lcovText); const diff = new Map([ ["/repo/packages/auth/src/entities/models/user.ts", new Set([1, 2, 3])], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "pass"); assert.equal(result.uncovered.length, 0); }); test("fails when a changed line is in lcov with count 0", () => { const lcov = parseLcov(lcovText); const diff = new Map([ [ "/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts", new Set([5, 6]), ], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "fail"); assert.equal(result.uncovered.length, 2); assert.deepEqual(result.uncovered.map((u) => u.line).sort(), [5, 6]); assert.ok(result.uncovered.every((u) => u.kind === "uncovered")); }); test("ignores lines without DA records (non-executable)", () => { const lcov = parseLcov(lcovText); // Line 4 has no DA record in the fixture const diff = new Map([ [ "/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts", new Set([4]), ], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "pass"); }); test("flags files with no coverage data (new untested file)", () => { const lcov = parseLcov(lcovText); const diff = new Map([ [ "packages/media/src/application/use-cases/upload.use-case.ts", new Set([1, 2, 3, 4, 5]), ], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "fail"); assert.equal(result.uncovered.length, 1); assert.equal(result.uncovered[0].kind, "no-coverage-data"); }); test("skips allowed extensions (.md, .json, .test.ts, configs)", () => { const lcov = parseLcov(lcovText); const diff = new Map([ ["CLAUDE.md", new Set([1, 2])], ["package.json", new Set([1])], ["packages/auth/vitest.config.ts", new Set([1])], [ "packages/auth/src/application/use-cases/sign-in.use-case.test.ts", new Set([11]), ], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "pass"); assert.equal(result.summary.filesGated, 0); assert.equal(result.summary.filesChanged, 4); }); test("skips .env template files (.env, .env.example, .env.local)", () => { const lcov = parseLcov(lcovText); const diff = new Map([ [".env.example", new Set([1, 2, 3])], [".env.local", new Set([1])], [".env", new Set([1])], ]); const result = computeDiffCoverage(diff, lcov); assert.equal(result.status, "pass"); assert.equal(result.summary.filesGated, 0); assert.equal(result.summary.filesChanged, 3); }); test("end-to-end fixture: mixed pass/fail/skip/no-data", () => { const lcov = parseLcov(lcovText); const diff = parseGitDiff(diffText); const result = computeDiffCoverage(diff, lcov, { repoRoot: "/repo" }); assert.equal(result.status, "fail"); // CLAUDE.md, sign-in.use-case.test.ts -> skipped (allowlist) // sign-in.use-case.ts lines 2,3,5,6 -> 5,6 are uncovered, 2,3 don't have DA records (not in lcov for those lines) so they don't count // user.ts line 2 -> covered (count: 1) // get-article.use-case.ts line 12 -> uncovered (count: 0) // upload.use-case.ts -> no coverage data // The expected uncovered set: sign-in lines 5,6 + get-article line 12 + upload (no-data) const uncoveredKinds = result.uncovered.map((u) => u.kind); assert.ok(uncoveredKinds.includes("no-coverage-data")); assert.ok(uncoveredKinds.includes("uncovered")); }); test("resolves repo-relative diff paths against lcov absolute paths", () => { const lcov = parseLcov(lcovText); const diff = new Map([ // Diff uses repo-relative; lcov has absolute. Suffix match should // bridge them. [ "packages/auth/src/application/use-cases/sign-in.use-case.ts", new Set([8]), ], ]); const result = computeDiffCoverage(diff, lcov, { repoRoot: "/repo" }); assert.equal(result.status, "pass"); }); });