Files
agentic-dev-template/scripts/coverage/aggregate.test.mjs
Danijel Martinek bd5a077227 feat(coverage): pnpm coverage:aggregate + L2 implementation
Lands L2 of the agent-first coverage architecture (ADR-020) — the
aggregated trend store.

Script: scripts/coverage/aggregate.mjs (zero-dep Node ESM)
  - discoverLcovs: walks packages/* and apps/* for coverage/lcov.info
  - normalizeLcov: rewrites SF entries from package-relative (vitest's
    output) to repo-relative, so the merged file matches git diff paths
  - summarizeLcov: computes statement/branch/function/line percentages
    from LF/LH/BRF/BRH/FNF/FNH summary records
  - aggregate: merges all lcovs and returns mergedLcov + summary
  - Writes coverage/lcov.info (gitignored — large) and
    coverage/summary.json (committed — trend via git log -- ...) with
    timestamp, short commit SHA, repo + per-package percentages

Test surface: scripts/coverage/aggregate.test.mjs (10 tests, all green)
  - Fixtures at __fixtures__/aggregate-pkg-a.lcov +
    aggregate-pkg-b.lcov (synthetic, structured to make percentages
    deterministic)
  - Covers: path normalization (prefix, absolute, double-prefix
    avoidance), summary computation (percentages, zero-division,
    rounding), discovery (packages + apps, missing dirs), full
    aggregation in a tmp repo

Wired:
  - root package.json adds "coverage:aggregate" script
  - .gitignore restructured: per-package coverage/ stays ignored,
    aggregated /coverage/ ignored EXCEPT summary.json (committed for
    trend) and .gitkeep markers

L1 allowlist fix folded in (scripts/coverage/diff.mjs):
  - The previous (^|/)coverage/ regex accidentally caught
    scripts/coverage/* — replaced with anchored patterns
    (^coverage/, ^packages/*/coverage/, ^apps/*/coverage/)
  - Allowlist scripts/ and turbo/generators/ since they're dev tooling
    tested via node --test, outside vitest's v8 lcov pipeline

Smoke-tested end-to-end:
  - pnpm coverage:aggregate merged 3 lcovs (auth + media + navigation
    from this session's earlier runs), repo coverage 95.22% statements
  - pnpm coverage:diff against HEAD~1 with the new merged lcov reports
    PASS — all 6 diff files correctly allowlisted

First committed snapshot of coverage/summary.json lands with this
commit, anchoring the trend history at this state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:10:22 +02:00

175 lines
6.0 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 os from "node:os";
import { fileURLToPath } from "node:url";
import {
discoverLcovs,
normalizeLcov,
summarizeLcov,
aggregate,
} from "./aggregate.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURES = path.join(__dirname, "__fixtures__");
const pkgA = fs.readFileSync(
path.join(FIXTURES, "aggregate-pkg-a.lcov"),
"utf8",
);
const pkgB = fs.readFileSync(
path.join(FIXTURES, "aggregate-pkg-b.lcov"),
"utf8",
);
describe("normalizeLcov", () => {
test("prefixes packageDir onto each SF line", () => {
const result = normalizeLcov(pkgA, "packages/auth");
assert.ok(result.includes("SF:packages/auth/src/foo.ts"));
assert.ok(result.includes("SF:packages/auth/src/bar.ts"));
assert.ok(!result.includes("SF:src/foo.ts\n")); // no unprefixed paths remain
});
test("leaves absolute paths untouched", () => {
const text = "SF:/absolute/path/foo.ts\nDA:1,5\nend_of_record";
const result = normalizeLcov(text, "packages/auth");
assert.ok(result.includes("SF:/absolute/path/foo.ts"));
});
test("doesn't double-prefix when already prefixed", () => {
const text = "SF:packages/auth/src/foo.ts\nDA:1,5\nend_of_record";
const result = normalizeLcov(text, "packages/auth");
assert.ok(result.includes("SF:packages/auth/src/foo.ts"));
assert.ok(!result.includes("SF:packages/auth/packages/auth/"));
});
test("preserves non-SF lines verbatim", () => {
const result = normalizeLcov(pkgA, "packages/auth");
assert.ok(result.includes("DA:1,5"));
assert.ok(result.includes("LH:2"));
assert.ok(result.includes("end_of_record"));
});
});
describe("summarizeLcov", () => {
test("computes percentages from LF/LH/BRF/BRH/FNF/FNH summary records", () => {
const summary = summarizeLcov(pkgA);
// LF=3+2=5, LH=2+2=4 -> 80% statements/lines
assert.equal(summary.statements, 80);
assert.equal(summary.lines, 80);
// BRF=2+0=2, BRH=1+0=1 -> 50% branches
assert.equal(summary.branches, 50);
// FNF=1+1=2, FNH=1+1=2 -> 100% functions
assert.equal(summary.functions, 100);
});
test("treats zero-found as 100% (avoids division by zero)", () => {
const text =
"SF:src/x.ts\nLF:0\nLH:0\nBRF:0\nBRH:0\nFNF:0\nFNH:0\nend_of_record";
const summary = summarizeLcov(text);
assert.equal(summary.statements, 100);
assert.equal(summary.branches, 100);
assert.equal(summary.functions, 100);
});
test("rounds percentages to 2 decimals", () => {
// LF=3, LH=2 -> 66.67%
const text = "SF:x\nLF:3\nLH:2\nend_of_record";
const summary = summarizeLcov(text);
assert.equal(summary.statements, 66.67);
});
});
describe("aggregate", () => {
test("returns null summary when no lcovs are found", () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cov-agg-empty-"));
try {
const result = aggregate(tmpRoot);
assert.equal(result.summary, null);
assert.equal(result.lcovs.length, 0);
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
test("merges multiple lcovs and emits per-package summary", () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cov-agg-merge-"));
try {
// Create packages/pkg-a + packages/pkg-b with their lcovs
fs.mkdirSync(path.join(tmpRoot, "packages", "pkg-a", "coverage"), {
recursive: true,
});
fs.mkdirSync(path.join(tmpRoot, "packages", "pkg-b", "coverage"), {
recursive: true,
});
fs.writeFileSync(
path.join(tmpRoot, "packages", "pkg-a", "coverage", "lcov.info"),
pkgA,
);
fs.writeFileSync(
path.join(tmpRoot, "packages", "pkg-b", "coverage", "lcov.info"),
pkgB,
);
// Synthetic package.json for name resolution
fs.writeFileSync(
path.join(tmpRoot, "packages", "pkg-a", "package.json"),
JSON.stringify({ name: "@repo/pkg-a" }),
);
fs.writeFileSync(
path.join(tmpRoot, "packages", "pkg-b", "package.json"),
JSON.stringify({ name: "@repo/pkg-b" }),
);
const result = aggregate(tmpRoot, { now: "2026-05-13T00:00:00Z" });
assert.equal(result.lcovs.length, 2);
assert.ok(result.mergedLcov.includes("SF:packages/pkg-a/src/foo.ts"));
assert.ok(result.mergedLcov.includes("SF:packages/pkg-b/src/baz.ts"));
// Per-package summaries
assert.ok(result.summary.byPackage["@repo/pkg-a"]);
assert.ok(result.summary.byPackage["@repo/pkg-b"]);
assert.equal(result.summary.byPackage["@repo/pkg-a"].statements, 80);
assert.equal(result.summary.byPackage["@repo/pkg-b"].statements, 75);
// Repo-level summary: lines hit 4+3=7 of 5+4=9 -> 77.78%
assert.equal(result.summary.repo.statements, 77.78);
assert.equal(result.summary.generatedAt, "2026-05-13T00:00:00Z");
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});
describe("discoverLcovs", () => {
test("finds lcovs under packages/* and apps/*", () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "cov-discover-"));
try {
fs.mkdirSync(path.join(tmpRoot, "packages", "p1", "coverage"), {
recursive: true,
});
fs.mkdirSync(path.join(tmpRoot, "apps", "a1", "coverage"), {
recursive: true,
});
// p2 has no coverage dir
fs.mkdirSync(path.join(tmpRoot, "packages", "p2"), { recursive: true });
fs.writeFileSync(
path.join(tmpRoot, "packages", "p1", "coverage", "lcov.info"),
"",
);
fs.writeFileSync(
path.join(tmpRoot, "apps", "a1", "coverage", "lcov.info"),
"",
);
const found = discoverLcovs(tmpRoot);
assert.equal(found.length, 2);
const dirs = found.map((f) => f.packageDir).sort();
assert.deepEqual(dirs, ["apps/a1", "packages/p1"]);
} finally {
fs.rmSync(tmpRoot, { recursive: true, force: true });
}
});
});