From b0840c914df9af9a86613182ece7259c56178eec Mon Sep 17 00:00:00 2001 From: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:47:47 -0400 Subject: [PATCH] test: put the gates' own top-level functions under the branch-coverage floor, ratcheted per file The floor read scripts/*.mjs and the inline page scripts but skipped every *.test.mjs, which is where most checker logic in this repository lives. Read the top-level function declarations of those gates with the same V8 data, confined to the declaration spans, and ratchet the 179 ranges found at 1d0251e per file through TEST_HELPER_BASELINE so the count can only fall. Closes #287 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com> --- README.md | 11 +- scripts/coverage-floor.test.mjs | 171 ++++++++++++++++++++++++++++++-- 2 files changed, 171 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8bd4419..ed409d7 100644 --- a/README.md +++ b/README.md @@ -218,9 +218,14 @@ red PR job against. source slice — so a branch that runs under the suite without a test asserting what it decides cannot merge green. A range counts as executed if any child process ran it; a tool's main-module guard (`if (process.argv[1] … import.meta.url)`) is excluded because - its self-test runs it via `spawnSync` against a copy. Each rule has a fixture - self-test, including real child runs over a tmp helper and a tmp page with one dead - branch each. Zero dependencies. + its self-test runs it via `spawnSync` against a copy. The same read covers the + top-level `function` declarations of every other `*.test.mjs` gate — where most checker + logic actually lives (`problemsOf`, `unguardedSmoothScroll`, …) — confined to those + declaration spans so a `test()` body is never a finding, and ratcheted per file through + `TEST_HELPER_BASELINE`: an entry may only be lowered (and is deleted at zero), a file + without one must be fully asserted, and a stale entry fails. Each rule has a fixture + self-test, including real child runs over a tmp helper, a tmp page and a tmp gate with + one dead branch each. Zero dependencies. - `node --test scripts/llms-txt.test.mjs` — static gate keeping `llms.txt` in step with `index.html`: the agent CLIs it names must equal the Agent CLIs chips, the inference engines/gateways it names must equal the engine and gateway chips (classifier chips diff --git a/scripts/coverage-floor.test.mjs b/scripts/coverage-floor.test.mjs index 7c5c51f..49f8dd9 100644 --- a/scripts/coverage-floor.test.mjs +++ b/scripts/coverage-floor.test.mjs @@ -27,6 +27,37 @@ const SELF = basename(fileURLToPath(import.meta.url)); // never recurse; the file list below already leaves it out. const CHILD_ENV = "COVERAGE_FLOOR_CHILD"; +// Dead ranges inside the top-level functions of each gate when the ratchet +// landed. Lower an entry when you assert a branch; delete it at zero; never +// raise one. The end state is an empty object and the same floor the +// scripts/*.mjs helpers already meet. +export const TEST_HELPER_BASELINE = { + "scripts/acmm-levels.test.mjs": 3, + "scripts/automation-docs.test.mjs": 4, + "scripts/availability-probes.test.mjs": 2, + "scripts/carousels.test.mjs": 6, + "scripts/ci-wiring.test.mjs": 6, + "scripts/dependabot-config.test.mjs": 9, + "scripts/issue-forms.test.mjs": 15, + "scripts/jsonld-contract.test.mjs": 14, + "scripts/llms-txt.test.mjs": 3, + "scripts/no-js-controls.test.mjs": 5, + "scripts/owners-file.test.mjs": 4, + "scripts/page-404.test.mjs": 5, + "scripts/page-images.test.mjs": 19, + "scripts/page-markup.test.mjs": 2, + "scripts/page-meta.test.mjs": 20, + "scripts/page-structure.test.mjs": 7, + "scripts/pages-build.test.mjs": 4, + "scripts/redirect-targets.test.mjs": 4, + "scripts/story-cards.test.mjs": 14, + "scripts/story-dialog.test.mjs": 1, + "scripts/story-form-contract.test.mjs": 12, + "scripts/style-contract.test.mjs": 13, + "scripts/workflow-docs.test.mjs": 4, + "scripts/workflow-hardening.test.mjs": 3, +}; + // --- analysis --------------------------------------------------------------- // [start, end) offsets of every main-module guard block in a tool's source: @@ -43,6 +74,24 @@ export function cliEntrySpans(src) { return spans; } +// [start, end) offsets of every top-level function declaration in a gate: +// `export function f(...) {` / `async function g(...) {` opening at the end of +// its line, up to the first `}` on a line of its own. Formatted code closes a +// top-level body exactly there, and counting braces would trip over the `{` +// inside the string and regex literals the checkers are made of. A one-line +// declaration has no span (it would otherwise run on to the next body). +export function functionSpans(src) { + const spans = []; + for (const m of src.matchAll(/^(?:export )?(?:async )?function\b[^\n]*\{$/gm)) { + const close = src.indexOf("\n}", m.index); + if (close < 0) break; + const after = src[close + 2]; + if (after !== undefined && after !== "\n") continue; + spans.push([m.index, close + 2]); + } + return spans; +} + // Execution count of the whole span [start, end) in one process: the count of // the innermost V8 range that encloses it entirely. V8 only emits a range for // a block whose count differs from its parent, so a block the process ran as @@ -59,7 +108,8 @@ export function countOf(functions, start, end) { // Ranges no process executed: zero-count in one run and still zero in every // other run that loaded the script. `runs` is one `functions` array per process. -export function deadRanges(src, runs) { +// With `within`, only ranges inside one of those [start, end) spans count. +export function deadRanges(src, runs, within) { const seen = new Map(); for (const functions of runs) { for (const fn of functions) { @@ -73,6 +123,7 @@ export function deadRanges(src, runs) { for (const [start, end] of seen.values()) { if (!src.slice(start, end).trim()) continue; if (skip.some(([s, e]) => start >= s && start < e)) continue; + if (within && !within.some(([s, e]) => start >= s && end <= e)) continue; if (runs.some((functions) => (countOf(functions, start, end) ?? 0) > 0)) continue; out.push([start, end]); } @@ -83,21 +134,44 @@ export function lineOf(src, offset) { return src.slice(0, offset).split("\n").length; } -// One problem per dead range, naming file, line and the source it starts with. -// A source is either the file's text or `{ src, line }` for a block embedded in -// a page at 1-based `line`, so the reported line is the page's. -export function problemsOf(sources, runsByFile) { - const problems = []; +// Problems per file, naming file, line and the source each dead range starts +// with. A source is either the file's text or `{ src, line, within }`: `line` +// for a block embedded in a page at that 1-based line, so the reported line is +// the page's; `within` to confine the search to those spans of the source. +export function problemsByFile(sources, runsByFile) { + const out = new Map(); for (const [file, entry] of [...sources].sort(([a], [b]) => a.localeCompare(b))) { - const { src, line } = typeof entry === "string" ? { src: entry, line: 1 } : entry; + const { src, line = 1, within } = typeof entry === "string" ? { src: entry } : entry; + const problems = []; + out.set(file, problems); const runs = runsByFile.get(file); if (!runs) { problems.push(`${file}: never loaded by the suite`); continue; } - for (const [start, end] of deadRanges(src, runs)) { + for (const [start, end] of deadRanges(src, runs, within)) { const slice = src.slice(start, end).trim().split("\n")[0]; const quoted = slice.length > 60 ? `${slice.slice(0, 57)}...` : slice; problems.push(`${file}:${lineOf(src, start) + line - 1} \`${quoted}\` never executed by the suite (add a test that asserts what this branch decides)`); } } + return out; +} + +// One problem per dead range, in file order. +export function problemsOf(sources, runsByFile) { + return [...problemsByFile(sources, runsByFile).values()].flat(); +} + +// Ratchet: a file may carry at most `baseline[file]` problems (0 when absent) +// and must lower its entry as they are fixed, so the baseline only shrinks. +export function ratchetProblems(byFile, baseline) { + const problems = []; + for (const [file, found] of byFile) { + const allowed = baseline[file] ?? 0; + if (found.length > allowed) problems.push(`${file}: ${found.length} dead ranges, baseline allows ${allowed}`, ...found); + else if (found.length < allowed) problems.push(`${file}: ${found.length} dead ranges left, baseline allows ${allowed} — lower TEST_HELPER_BASELINE to ${found.length || "nothing (delete the entry)"}`); + } + for (const file of Object.keys(baseline)) { + if (!byFile.has(file)) problems.push(`${file}: in TEST_HELPER_BASELINE but not a gate under scripts/ — delete the entry`); + } return problems; } @@ -111,6 +185,20 @@ export function helperSources(root) { .map((n) => [`scripts/${n}`, readFileSync(join(dir, n), "utf8")])); } +// The gates' own top-level functions: every scripts/*.test.mjs (this file +// aside — the child never loads it) that declares at least one, as +// `{ src, within }` so only those declarations are read. +export function testHelperSources(root, self = SELF) { + const dir = join(root, "scripts"); + const out = new Map(); + for (const n of readdirSync(dir).filter((n) => n.endsWith(".test.mjs") && n !== self).sort()) { + const src = readFileSync(join(dir, n), "utf8"); + const within = functionSpans(src); + if (within.length) out.set(`scripts/${n}`, { src, within }); + } + return out; +} + // Parsed NODE_V8_COVERAGE reports reduced to Map. export function runsFromReports(reports, root, sources) { const prefix = pathToFileURL(join(resolve(root), "scripts")).href + "/"; @@ -321,6 +409,65 @@ test("fixture: a real child run reports the dead branch of a tmp page's inline s } }); +test("fixture: functionSpans finds top-level declarations only and deadRanges honours within", () => { + const src = 'import x from "y";\nexport function a(v) {\n return v ? "{" : "}";\n}\n\nasync function b() {\n if (c) {\n d();\n }\n}\nfunction notClosed() { return 1; }\ntest("t", () => {\n const f = function () { return 2; };\n return f;\n});\n'; + const spans = functionSpans(src); + assert.deepEqual(spans.map(([s, e]) => src.slice(s, e).split("\n")[0]), ["export function a(v) {", "async function b() {"]); + assert.equal(src.slice(...spans[0]).at(-1), "}"); + assert.equal(src.slice(...spans[1]).at(-1), "}"); + assert.deepEqual(functionSpans("export function f() {\n return 1;\n}"), [[0, 35]]); + assert.deepEqual(functionSpans("export function f() { return 1; }\n"), []); + assert.deepEqual(functionSpans("function f() { return 1; }\ntest(\"t\", () => {\n f();\n});\nfunction g() {\n return 2;\n}\n").map(([s]) => s), [56]); + assert.deepEqual(functionSpans("export function f() {\n return 1;\n});\n"), []); + assert.deepEqual(functionSpans("export function f() {\n return 1;"), []); + assert.deepEqual(functionSpans("const x = 1;\n"), []); + const inA = src.indexOf(': "}"'); + const inTest = src.indexOf("return 2;"); + const runs = [[{ functionName: "", ranges: [{ startOffset: 0, endOffset: src.length, count: 1 }, { startOffset: inA, endOffset: inA + 5, count: 0 }, { startOffset: inTest, endOffset: inTest + 9, count: 0 }], isBlockCoverage: true }]]; + assert.deepEqual(deadRanges(src, runs), [[inA, inA + 5], [inTest, inTest + 9]]); + assert.deepEqual(deadRanges(src, runs, spans), [[inA, inA + 5]]); + assert.deepEqual(problemsOf(new Map([["scripts/g.test.mjs", { src, within: spans }]]), new Map([["scripts/g.test.mjs", runs]])), [ + 'scripts/g.test.mjs:3 `: "}"` never executed by the suite (add a test that asserts what this branch decides)', + ]); +}); + +test("fixture: the ratchet fails on growth, on a stale entry and on an unlowered one, and passes at the baseline", () => { + const byFile = new Map([["scripts/a.test.mjs", ["a:1 dead", "a:2 dead"]], ["scripts/b.test.mjs", []], ["scripts/c.test.mjs", ["c:1 dead"]]]); + assert.deepEqual(ratchetProblems(byFile, { "scripts/a.test.mjs": 2, "scripts/c.test.mjs": 1 }), []); + assert.deepEqual(ratchetProblems(byFile, { "scripts/a.test.mjs": 1, "scripts/c.test.mjs": 1 }), ["scripts/a.test.mjs: 2 dead ranges, baseline allows 1", "a:1 dead", "a:2 dead"]); + assert.deepEqual(ratchetProblems(byFile, { "scripts/a.test.mjs": 2 }), ["scripts/c.test.mjs: 1 dead ranges, baseline allows 0", "c:1 dead"]); + assert.deepEqual(ratchetProblems(byFile, { "scripts/a.test.mjs": 3, "scripts/b.test.mjs": 1, "scripts/c.test.mjs": 1 }), [ + "scripts/a.test.mjs: 2 dead ranges left, baseline allows 3 — lower TEST_HELPER_BASELINE to 2", + "scripts/b.test.mjs: 0 dead ranges left, baseline allows 1 — lower TEST_HELPER_BASELINE to nothing (delete the entry)", + ]); + assert.deepEqual(ratchetProblems(byFile, { "scripts/a.test.mjs": 2, "scripts/c.test.mjs": 1, "scripts/gone.test.mjs": 1 }), ["scripts/gone.test.mjs: in TEST_HELPER_BASELINE but not a gate under scripts/ — delete the entry"]); + assert.deepEqual(ratchetProblems(new Map([["scripts/n.test.mjs", ["scripts/n.test.mjs: never loaded by the suite"]]]), {}), ["scripts/n.test.mjs: 1 dead ranges, baseline allows 0", "scripts/n.test.mjs: never loaded by the suite"]); + assert.ok(Object.values(TEST_HELPER_BASELINE).every((n) => Number.isInteger(n) && n > 0), "baseline entries are positive integers; delete an entry at zero"); +}); + +test("fixture: a real child run reads a gate's own top-level functions and nothing inside its test bodies", () => { + const dir = mkdtempSync(join(tmpdir(), "coverage-floor-gate-")); + try { + mkdirSync(join(dir, "scripts")); + writeFileSync(join(dir, "scripts", "gate.test.mjs"), 'import { test } from "node:test";\nimport assert from "node:assert/strict";\nexport function pick(x) {\n return x ? "yes" : "no";\n}\ntest("yes", () => {\n assert.equal(pick(1), "yes");\n if (!pick(1)) assert.fail("unreachable");\n});\n'); + writeFileSync(join(dir, "scripts", "plain.test.mjs"), 'import { test } from "node:test";\ntest("plain", () => {});\n'); + writeFileSync(join(dir, "scripts", "coverage-floor.test.mjs"), 'import { test } from "node:test";\nexport function ignored(x) {\n return x ? 1 : 2;\n}\ntest("self", () => {});\n'); + const sources = testHelperSources(dir); + assert.deepEqual([...sources.keys()], ["scripts/gate.test.mjs"]); + assert.deepEqual([...testHelperSources(dir, "nothing.test.mjs").keys()], ["scripts/coverage-floor.test.mjs", "scripts/gate.test.mjs"]); + const { reports, status, stdout, stderr } = collect(dir, suiteFiles(dir)); + assert.equal(status, 0, `${stdout}\n${stderr}`); + const byFile = problemsByFile(sources, runsFromReports(reports, dir, sources)); + assert.deepEqual([...byFile], [["scripts/gate.test.mjs", [ + 'scripts/gate.test.mjs:4 `: "no"` never executed by the suite (add a test that asserts what this branch decides)', + ]]]); + assert.deepEqual(ratchetProblems(byFile, { "scripts/gate.test.mjs": 1 }), []); + assert.equal(ratchetProblems(byFile, {}).length, 2); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}); + // --- live gate -------------------------------------------------------------- test("every branch of every scripts/*.mjs helper and every inline page script is executed by the suite", { skip: process.env[CHILD_ENV] ? "running inside the coverage-floor child" : false }, () => { @@ -340,6 +487,14 @@ test("every branch of every scripts/*.mjs helper and every inline page script is ], []); }); +test("every branch of every gate's own top-level functions is executed by the suite, down to TEST_HELPER_BASELINE", { skip: process.env[CHILD_ENV] ? "running inside the coverage-floor child" : false }, () => { + const sources = testHelperSources(ROOT); + assert.ok(sources.size > 0, "no gate under scripts/ declares a top-level function"); + const { reports, status, stdout, stderr } = collect(ROOT, suiteFiles(ROOT)); + assert.equal(status, 0, `child suite failed; fix it first\n${stdout}\n${stderr}`); + assert.deepEqual(ratchetProblems(problemsByFile(sources, runsFromReports(reports, ROOT, sources)), TEST_HELPER_BASELINE), []); +}); + test("fixture: real inline child coverage maps Function wrappers to original page lines", () => { const dir = mkdtempSync(join(tmpdir(), "coverage-inline-fixture-")); try {