Files
agentic-dev-template/scripts/coverage/diff.test.mjs
Danijel Martinek d67a89179e fix(scripts): exempt .env template files from diff-coverage gate
.env.example and siblings are config/template files with no executable
code. The coverage:diff script now matches them via the same dotfile
pattern used for .gitignore and .npmrc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 18:22:19 +00:00

201 lines
6.9 KiB
JavaScript

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");
});
});