Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions scripts/coverage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,35 @@ if (minRaw !== undefined && (!Number.isFinite(min) || min < 0 || min > 100)) {
process.exit(2)
}
const reportDir = path.join(repoRoot, ".headlesscode", "coverage")
// PID-unique temp dir: c8 wipes its temp dir at startup, and this script
// runs nested inside itself (coverage.test.mjs spawns filtered coverage
// runs as part of the suite). A shared tmp/ meant each nested run deleted
// every profile the outer run had collected, collapsing full-suite line
// coverage to ~4.5%. Unique dirs also let concurrent runs coexist.
const tempDir = path.join(reportDir, `tmp-${process.pid}`)

const testArgs = ["scripts/run-tests.mjs"]
if (filter) testArgs.push("--filter", filter)
const result = spawnSync("node_modules/.bin/c8", [
"--reporter=text",
"--reporter=json-summary",
"--reports-dir", reportDir,
"--temp-directory", tempDir,
"--all",
"--src", "src",
"--exclude", "**/__tests__/**",
"--exclude", "**/*.test.ts",
"--exclude", "src/vendor/**",
"node", ...testArgs,
], { cwd: repoRoot, stdio: "inherit", env: process.env })
// Raw V8 profiles are tens of MB per run; the report is already written by
// now, so remove them on every exit path (pass AND fail) to avoid pile-up.
try {
fs.rmSync(tempDir, { recursive: true, force: true })
} catch {
// best-effort — a leftover tmp dir is a disk-space nit, never worth
// failing the coverage run over.
}
if (result.error) {
console.error(`coverage: failed to run c8: ${result.error.message}`)
process.exit(1)
Expand Down
21 changes: 20 additions & 1 deletion scripts/coverage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// percentage report on a tiny filtered run (claims tests, a few seconds)
// instead of the full suite. Exit 1 with a clear message on any mismatch.
import { execFileSync } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"

const repoRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
const reportDir = path.join(repoRoot, ".headlesscode", "coverage")

function runCoverage(args) {
try {
Expand Down Expand Up @@ -34,4 +36,21 @@ if (floor.status === 0) fail("--min 100 unexpectedly passed")
if (!/below the 100% floor/.test(floor.out)) fail("floor-failure message missing")
const invalid = runCoverage(["--min", "abc"])
if (invalid.status !== 2) fail(`invalid --min exited ${invalid.status}, want 2`)
console.log(" ok coverage-selftest: summary line, floor pass/fail, invalid arg")
// Nested-run isolation (2026-10-13 incident: nested c8 runs wiped the
// outer run's shared tmp/, collapsing full-suite coverage to ~4.5%).
// Each finished run must clean its own pid-unique tmp dir; a shared bare
// tmp/ must never come back. Pre-existing tmp-* dirs belong to still-live
// outer/concurrent runs, so only NEW leftovers fail.
const before = new Set(fs.existsSync(reportDir) ? fs.readdirSync(reportDir) : [])
runCoverage([...filter, "--min", "0"])
const after = fs.existsSync(reportDir) ? fs.readdirSync(reportDir) : []
if (after.includes("tmp")) fail("shared bare tmp/ dir present — nested runs would wipe it")
for (const entry of after) {
if (entry.startsWith("tmp-") && !before.has(entry)) {
fail(`leftover temp dir ${entry} from a finished nested run`)
}
if (entry.startsWith("tmp-") && !/^tmp-\d+$/.test(entry)) {
fail(`non-pid temp dir ${entry}`)
}
}
console.log(" ok coverage-selftest: summary line, floor pass/fail, invalid arg, nested-run tmp isolation")
Loading