From 2085dba6711071fb262abae75f96c8bf04248cc0 Mon Sep 17 00:00:00 2001
From: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
Date: Sat, 24 Oct 2026 10:23:21 -0400
Subject: [PATCH] test: add branch-coverage floor over scripts/*.mjs helpers
Every quality fix in this repository since early October has been the
same change: a test asserting a helper branch that already ran under the
suite but was never checked (#232, #241, #247). Nothing failed when a new
branch was added to a helper without an assertion, so the class recurred.
scripts/coverage-floor.test.mjs runs the rest of the suite as a child
with NODE_V8_COVERAGE, merges the raw V8 block-coverage ranges for
scripts/*.mjs (not *.test.mjs) across processes, and fails naming each
range no process executed: file, line and the quoted source slice. A
tool's main-module guard is excluded because its self-test runs it via
spawnSync against a copy. Fixture self-tests cover the cross-process
merge, the exclusions, the message shape and a real child run over a tmp
helper with one dead branch.
The ranges the gate found at HEAD are asserted in the same change:
page-csp.mjs parseCsp without
, a contentless CSP meta (null
content offset/quote), empty directive segments, script-src with no
default-src, neither img-src nor default-src,
without src; and
update-csp-hashes.mjs refusing a contentless policy meta.
Closes #251
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 | 230 +++++++++++++++++++++++++++++
scripts/page-csp.test.mjs | 51 ++++++-
scripts/update-csp-hashes.test.mjs | 5 +
4 files changed, 296 insertions(+), 1 deletion(-)
create mode 100644 scripts/coverage-floor.test.mjs
diff --git a/README.md b/README.md
index 0505662..468bcdf 100644
--- a/README.md
+++ b/README.md
@@ -201,6 +201,17 @@ red PR job against.
Then pins the live tree: every committed content page is already up to date, so the
tool and `page-csp.test.mjs` agree on what correct means, and a failure names the
regenerate command. Zero dependencies.
+- `node --test scripts/coverage-floor.test.mjs` — branch-coverage floor over the
+ `scripts/*.mjs` helpers the other gates import (`page-csp.mjs`, `html-pages.mjs`,
+ `actions-if.mjs`, `update-csp-hashes.mjs`): runs the rest of the suite once more as a
+ child with `NODE_V8_COVERAGE`, reads the raw V8 block-coverage ranges for every helper
+ (`*.test.mjs` excluded) and fails naming each range no process executed — file, line
+ and the quoted source slice — so a helper 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 a real child run over a tmp helper
+ with one dead branch. 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
new file mode 100644
index 0000000..f69c44c
--- /dev/null
+++ b/scripts/coverage-floor.test.mjs
@@ -0,0 +1,230 @@
+#!/usr/bin/env node
+// Branch-coverage floor over the scripts/*.mjs helpers. Every other gate here
+// is a *.test.mjs that imports a helper (page-csp.mjs, html-pages.mjs, ...),
+// so a branch added to a helper runs under the suite the moment any page hits
+// it — and nothing fails when no test asserts what that branch decides. This
+// gate runs the rest of the suite once more as a child with NODE_V8_COVERAGE,
+// reads the raw V8 block-coverage ranges for scripts/*.mjs (not *.test.mjs),
+// and fails naming each range the suite never executed: file, line and the
+// quoted source slice, so the fix is a test rather than arithmetic. The
+// main-module guard of a tool (`if (process.argv[1] ... import.meta.url) {`)
+// is excluded: its self-test runs it via spawnSync against a copy, which V8
+// attributes to the copy. Each rule has a fixture self-test. Zero dependencies.
+// Usage: node --test scripts/coverage-floor.test.mjs
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { basename, dirname, join, relative, resolve } from "node:path";
+import { fileURLToPath, pathToFileURL } from "node:url";
+import { spawnSync } from "node:child_process";
+
+const HERE = dirname(fileURLToPath(import.meta.url));
+const ROOT = join(HERE, "..");
+const SELF = basename(fileURLToPath(import.meta.url));
+// Set in the child so a stray `coverage-floor.test.mjs` in its file list can
+// never recurse; the file list below already leaves it out.
+const CHILD_ENV = "COVERAGE_FLOOR_CHILD";
+
+// --- analysis ---------------------------------------------------------------
+
+// [start, end) offsets of every main-module guard block in a tool's source:
+// `if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { ... }`.
+export function cliEntrySpans(src) {
+ const spans = [];
+ for (const m of src.matchAll(/^if \(process\.argv\[1\][^\n]*import\.meta\.url\)\) \{/gm)) {
+ let depth = 0;
+ for (let i = m.index + m[0].length - 1; i < src.length; i++) {
+ if (src[i] === "{") depth++;
+ else if (src[i] === "}" && --depth === 0) { spans.push([m.index, i + 1]); break; }
+ }
+ }
+ 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
+// often as its parent has no range of its own and inherits the parent's count.
+export function countOf(functions, start, end) {
+ let best;
+ for (const fn of functions) {
+ for (const r of fn.ranges) {
+ if (r.startOffset <= start && r.endOffset >= end && (!best || r.endOffset - r.startOffset < best.endOffset - best.startOffset)) best = r;
+ }
+ }
+ return best?.count;
+}
+
+// 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) {
+ const seen = new Map();
+ for (const functions of runs) {
+ for (const fn of functions) {
+ for (const r of fn.ranges) {
+ if (r.count === 0) seen.set(`${r.startOffset}:${r.endOffset}`, [r.startOffset, r.endOffset]);
+ }
+ }
+ }
+ const skip = cliEntrySpans(src);
+ const out = [];
+ 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 (runs.some((functions) => (countOf(functions, start, end) ?? 0) > 0)) continue;
+ out.push([start, end]);
+ }
+ return out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
+}
+
+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.
+export function problemsOf(sources, runsByFile) {
+ const problems = [];
+ for (const [file, src] of [...sources].sort(([a], [b]) => a.localeCompare(b))) {
+ const runs = runsByFile.get(file);
+ if (!runs) { problems.push(`${file}: never loaded by the suite`); continue; }
+ for (const [start, end] of deadRanges(src, runs)) {
+ 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)} \`${quoted}\` never executed by the suite (add a test that asserts what this branch decides)`);
+ }
+ }
+ return problems;
+}
+
+// --- collection -------------------------------------------------------------
+
+// Helper sources under /scripts: *.mjs that are not *.test.mjs, keyed by
+// repository-relative path.
+export function helperSources(root) {
+ const dir = join(root, "scripts");
+ return new Map(readdirSync(dir).filter((n) => n.endsWith(".mjs") && !n.endsWith(".test.mjs")).sort()
+ .map((n) => [`scripts/${n}`, readFileSync(join(dir, n), "utf8")]));
+}
+
+// Parsed NODE_V8_COVERAGE reports reduced to Map.
+export function runsFromReports(reports, root, sources) {
+ const prefix = pathToFileURL(join(resolve(root), "scripts")).href + "/";
+ const out = new Map();
+ for (const report of reports) {
+ for (const script of report.result ?? []) {
+ if (!script.url.startsWith(prefix)) continue;
+ const file = relative(resolve(root), fileURLToPath(script.url)).split("\\").join("/");
+ if (!sources.has(file)) continue;
+ if (!out.has(file)) out.set(file, []);
+ out.get(file).push(script.functions);
+ }
+ }
+ return out;
+}
+
+// Runs `node --test ` from with coverage into a temp dir; returns
+// the parsed reports and the child's status.
+export function collect(root, files) {
+ const dir = mkdtempSync(join(tmpdir(), "coverage-floor-"));
+ try {
+ // The parent runner marks its own children with NODE_TEST_CONTEXT; a child
+ // `node --test` that inherits it reports over IPC instead of running files.
+ const { NODE_TEST_CONTEXT: _ctx, ...env } = process.env;
+ const child = spawnSync(process.execPath, ["--test", ...files], {
+ cwd: root, encoding: "utf8", env: { ...env, NODE_V8_COVERAGE: dir, [CHILD_ENV]: "1" }, maxBuffer: 64 * 1024 * 1024,
+ });
+ const reports = readdirSync(dir).filter((n) => n.endsWith(".json")).map((n) => JSON.parse(readFileSync(join(dir, n), "utf8")));
+ return { reports, status: child.status, stdout: child.stdout, stderr: child.stderr };
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+}
+
+export function suiteFiles(root) {
+ return readdirSync(join(root, "scripts")).filter((n) => n.endsWith(".test.mjs") && n !== SELF).sort().map((n) => `scripts/${n}`);
+}
+
+// --- fixture self-tests -----------------------------------------------------
+
+// Synthetic V8 `functions` arrays: a 40-char module whose block [20, 30) is
+// the branch under test.
+const SRC = "const a = 1;\nif (a) { b(); } else { c(); }";
+const FULL = [{ functionName: "", ranges: [{ startOffset: 0, endOffset: SRC.length, count: 1 }], isBlockCoverage: true }];
+const DEAD_ELSE = [{ functionName: "", ranges: [{ startOffset: 0, endOffset: SRC.length, count: 1 }, { startOffset: 27, endOffset: 42, count: 0 }], isBlockCoverage: true }];
+const DEAD_THEN = [{ functionName: "", ranges: [{ startOffset: 0, endOffset: SRC.length, count: 1 }, { startOffset: 20, endOffset: 27, count: 0 }], isBlockCoverage: true }];
+
+test("fixture: a range zero in one run but inherited as executed in another is not dead", () => {
+ assert.deepEqual(deadRanges(SRC, [DEAD_ELSE, FULL]), []);
+ assert.deepEqual(deadRanges(SRC, [DEAD_ELSE, DEAD_THEN]), []);
+ assert.deepEqual(deadRanges(SRC, [DEAD_ELSE, DEAD_ELSE]), [[27, 42]]);
+ assert.deepEqual(deadRanges(SRC, [DEAD_ELSE]), [[27, 42]]);
+ assert.equal(countOf(DEAD_ELSE, 27, 42), 0);
+ assert.equal(countOf(FULL, 27, 42), 1);
+ assert.equal(countOf([], 0, 1), undefined);
+});
+
+test("fixture: problems name file, line and the first line of the dead source, sorted by file then offset", () => {
+ const src = "export function f(x) {\n return x\n ? 1\n : 2;\n}\n";
+ const runs = [[{ functionName: "f", ranges: [{ startOffset: 0, endOffset: src.length, count: 1 }, { startOffset: src.indexOf(": 2"), endOffset: src.indexOf(";"), count: 0 }, { startOffset: src.indexOf("? 1"), endOffset: src.indexOf("\n :"), count: 0 }], isBlockCoverage: true }]];
+ const sources = new Map([["scripts/z.mjs", src], ["scripts/a.mjs", src]]);
+ assert.deepEqual(problemsOf(sources, new Map([["scripts/z.mjs", runs], ["scripts/a.mjs", runs]])), [
+ "scripts/a.mjs:3 `? 1` never executed by the suite (add a test that asserts what this branch decides)",
+ "scripts/a.mjs:4 `: 2` never executed by the suite (add a test that asserts what this branch decides)",
+ "scripts/z.mjs:3 `? 1` never executed by the suite (add a test that asserts what this branch decides)",
+ "scripts/z.mjs:4 `: 2` never executed by the suite (add a test that asserts what this branch decides)",
+ ]);
+ assert.deepEqual(problemsOf(new Map([["scripts/q.mjs", src]]), new Map()), ["scripts/q.mjs: never loaded by the suite"]);
+ const long = `x(${"a".repeat(80)})`;
+ const longRuns = [[{ functionName: "", ranges: [{ startOffset: 0, endOffset: long.length, count: 1 }, { startOffset: 0, endOffset: long.length - 1, count: 0 }], isBlockCoverage: true }]];
+ const [problem] = problemsOf(new Map([["scripts/l.mjs", long]]), new Map([["scripts/l.mjs", longRuns]]));
+ assert.match(problem, /`x\(a{55}\.\.\.` never executed/);
+});
+
+test("fixture: whitespace-only ranges and the main-module guard block are excluded", () => {
+ const src = 'import { resolve } from "node:path";\nimport { fileURLToPath } from "node:url";\nexport function f() { return 1; }\nif (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {\n if (f()) { g(); }\n}\n';
+ const guard = src.indexOf("if (process.argv[1]");
+ assert.deepEqual(cliEntrySpans(src), [[guard, src.length - 1]]);
+ assert.deepEqual(cliEntrySpans("export const x = 1;\n"), []);
+ const runs = [[
+ { functionName: "", ranges: [{ startOffset: 0, endOffset: src.length, count: 1 }, { startOffset: guard + src.slice(guard).indexOf("{"), endOffset: src.length - 1, count: 0 }, { startOffset: src.indexOf("\n", guard), endOffset: src.indexOf("\n", guard) + 2, count: 0 }], isBlockCoverage: true },
+ { functionName: "f", ranges: [{ startOffset: src.indexOf("function f"), endOffset: src.indexOf("}\nif") + 1, count: 0 }], isBlockCoverage: true },
+ ]];
+ assert.deepEqual(deadRanges(src, runs).map(([s]) => lineOf(src, s)), [3]);
+});
+
+test("fixture: a real child run reports exactly the dead branch of a tmp helper and nothing for a fully asserted one", () => {
+ const dir = mkdtempSync(join(tmpdir(), "coverage-floor-fixture-"));
+ try {
+ mkdirSync(join(dir, "scripts"));
+ writeFileSync(join(dir, "scripts", "dead.mjs"), "export function pick(x) {\n return x ? 'yes' : 'no';\n}\n");
+ writeFileSync(join(dir, "scripts", "dead.test.mjs"), 'import { test } from "node:test";\nimport assert from "node:assert/strict";\nimport { pick } from "./dead.mjs";\ntest("yes", () => assert.equal(pick(1), "yes"));\n');
+ writeFileSync(join(dir, "scripts", "live.mjs"), "export function pick(x) {\n return x ? 'yes' : 'no';\n}\n");
+ writeFileSync(join(dir, "scripts", "live.test.mjs"), 'import { test } from "node:test";\nimport assert from "node:assert/strict";\nimport { pick } from "./live.mjs";\ntest("both", () => { assert.equal(pick(1), "yes"); assert.equal(pick(0), "no"); });\n');
+ writeFileSync(join(dir, "scripts", "coverage-floor.test.mjs"), 'import { test } from "node:test";\ntest("self", () => {});\n');
+ const sources = helperSources(dir);
+ assert.deepEqual([...sources.keys()], ["scripts/dead.mjs", "scripts/live.mjs"]);
+ assert.deepEqual(suiteFiles(dir), ["scripts/dead.test.mjs", "scripts/live.test.mjs"]);
+ const { reports, status } = collect(dir, suiteFiles(dir));
+ assert.equal(status, 0);
+ const runs = runsFromReports(reports, dir, sources);
+ assert.deepEqual([...runs.keys()].sort(), ["scripts/dead.mjs", "scripts/live.mjs"]);
+ assert.deepEqual(problemsOf(sources, runs), [
+ "scripts/dead.mjs:2 `: 'no'` never executed by the suite (add a test that asserts what this branch decides)",
+ ]);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+});
+
+// --- live gate --------------------------------------------------------------
+
+test("every branch of every scripts/*.mjs helper is executed by the suite", { skip: process.env[CHILD_ENV] ? "running inside the coverage-floor child" : false }, () => {
+ const sources = helperSources(ROOT);
+ assert.ok(sources.size > 0, "no helpers under scripts/");
+ const files = suiteFiles(ROOT);
+ assert.ok(!files.includes(`scripts/${SELF}`));
+ const { reports, status, stdout, stderr } = collect(ROOT, files);
+ assert.equal(status, 0, `child suite failed; fix it first\n${stdout}\n${stderr}`);
+ assert.deepEqual(problemsOf(sources, runsFromReports(reports, ROOT, sources)), []);
+});
diff --git a/scripts/page-csp.test.mjs b/scripts/page-csp.test.mjs
index 66f298b..3364755 100644
--- a/scripts/page-csp.test.mjs
+++ b/scripts/page-csp.test.mjs
@@ -14,7 +14,7 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { relative } from "node:path";
import { ROOT, htmlFiles, isRedirectPage } from "./html-pages.mjs";
-import { REGEN_HINT, inlineScriptSources, problemsOf, sha256Source } from "./page-csp.mjs";
+import { REGEN_HINT, externalImageOrigins, inlineScriptSources, parseCsp, problemsOf, sha256Source } from "./page-csp.mjs";
// --- per-page gates ---------------------------------------------------------
@@ -119,6 +119,55 @@ test("fixture: 'unsafe-inline' and 'unsafe-eval' in script-src are rejected", ()
assert.deepEqual(problems, ["script-src must not contain 'unsafe-inline'", "script-src must not contain 'unsafe-eval'"]);
});
+test("fixture: a page without has no policy", () => {
+ assert.equal(parseCsp("x
"), null);
+ assert.deepEqual(problemsOf("x
"), ['missing ']);
+});
+
+test("fixture: a CSP meta without a content attribute parses to no directives and a null content offset", () => {
+ const html = fixture({ csp: null }).replace("", '');
+ const csp = parseCsp(html);
+ assert.deepEqual(csp.directives, {});
+ assert.equal(csp.count, 1);
+ assert.equal(csp.content, null);
+ assert.equal(csp.contentAt, -1);
+ assert.equal(csp.contentQuote, null);
+ assert.deepEqual(problemsOf(html), [
+ "script-src directive missing",
+ "base-uri must be 'none' or 'self'",
+ "form-action directive missing",
+ ]);
+});
+
+test("fixture: empty directive segments (`;;`, trailing `;`) are ignored", () => {
+ const h = sha256Source("console.log(1)");
+ const html = fixture({ csp: `${BASE};; script-src ${h};` });
+ assert.deepEqual(Object.keys(parseCsp(html).directives), ["default-src", "img-src", "base-uri", "form-action", "script-src"]);
+ assert.deepEqual(problemsOf(html), []);
+});
+
+test("fixture: script-src present but default-src missing is reported", () => {
+ const h = sha256Source("console.log(1)");
+ assert.deepEqual(problemsOf(fixture({ csp: `img-src 'self'; base-uri 'none'; form-action 'none'; script-src ${h}` })), [
+ "default-src must be 'none' so every fetch type is allow-listed explicitly",
+ ]);
+});
+
+test("fixture: with neither img-src nor default-src, an external
origin is not allowed", () => {
+ const h = sha256Source("console.log(1)");
+ const img = '
';
+ assert.deepEqual(problemsOf(fixture({ csp: `base-uri 'none'; form-action 'none'; script-src ${h}`, img })), [
+ "default-src must be 'none' so every fetch type is allow-listed explicitly",
+ "img-src does not allow external
origin https://github.com",
+ ]);
+});
+
+test("fixture:
without src and relative src contribute no external origin", () => {
+ assert.deepEqual(externalImageOrigins('![]()

'), ["https://example.org"]);
+ const h = sha256Source("console.log(1)");
+ assert.deepEqual(problemsOf(fixture({ csp: `${BASE}; script-src ${h}`, img: '
' })), []);
+});
+
test("fixture: inline event handler requires 'unsafe-hashes' plus its hash", () => {
const h = sha256Source("console.log(1)");
const hh = sha256Source("this.hidden=true");
diff --git a/scripts/update-csp-hashes.test.mjs b/scripts/update-csp-hashes.test.mjs
index c8e0141..b70863d 100644
--- a/scripts/update-csp-hashes.test.mjs
+++ b/scripts/update-csp-hashes.test.mjs
@@ -80,6 +80,11 @@ test("does not relax unsafe policy tokens or invent missing policies", () => {
assert.throws(() => updateCspHashes(fixture({ policy: BASE })), /exactly one script-src/);
assert.throws(() => updateCspHashes(fixture({ policy: `${BASE}; script-src 'self'; script-src 'none'` })), /exactly one script-src/);
assert.throws(() => updateCspHashes(fixture().replace("", '')), /exactly one CSP/);
+ // A single policy meta with no content attribute at all: parseCsp reports
+ // content === null and the tool must refuse rather than invent a policy.
+ const contentless = fixture().replace(/]*>/, '');
+ assert.equal(parseCsp(contentless).content, null);
+ assert.throws(() => updateCspHashes(contentless), /exactly one CSP meta with a content attribute/);
});
test("live tree: every committed content page is already up to date", () => {