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