Skip to content

test: hold each root script to a coverage floor - #841

Merged
allxsmith merged 7 commits into
mainfrom
test/724-script-coverage
Oct 1, 2026
Merged

allxsmith merged 7 commits into
mainfrom
test/724-script-coverage

Conversation

@allxsmith

@allxsmith allxsmith commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Closes #724.

The root node --test suite ran with no coverage, so nothing noticed when a script's tests stopped reaching part of it. This adds the report and a gate, in the shape the numbers asked for.

pnpm test:scripts runs the suite with Node's coverage on, limited to scripts/** (test files excluded), and prints the coverage table on every run. A second reporter, scripts/coverage-floors.mjs, compares each file with its row in scripts/coverage-floors.json and fails the run when:

  • a file drops below its floor on lines, branches or functions;
  • a file the suite loaded has no row (the message prints the row to paste);
  • a row names a file no test loaded, so deleting a test sibling can't quietly take its script out of the gate.

pnpm test and pnpm all both call it, so CI's existing Test step gates it with no workflow change.

Why per file. The issue suspected one threshold was the wrong shape, and the numbers agree. Across scripts/ the total sits around 85% lines, 91% branches and 87% functions, but files range from 17% lines (gen-api-docs.mjs, whose main() rewrites the docs) to 100% (the parsers). check-conformance.mjs alone is a quarter of the measured lines, so a small file losing half its coverage barely moves the total. I checked the #716 case directly: one uncovered function added to scripts/lib/shell-words.mjs takes that file to 75% functions and moves the total from 87.28% to 87.14%. The per-file gate fails on it, and no total that passes today would. Node's --test-coverage-* flags only judge the total, so the per-file check is a small reporter, with no new dependency.

Where the floors sit. Lines and functions sit at the file's worst run, rounded down to a whole percent. Branches get a small allowance: two fewer covered branches than the worst run, over the largest total seen. That's because the only noise measured, across runs on Node 22, 24 and 25 and CI's Linux run, is V8 sometimes counting a never-run fallback in render-triage-comment.mjs as a branch. Lines and functions never moved, and an allowance there would cost a third of a six-function file. Metrics at 100% keep a floor of 100, since a fully covered metric has no never-run code to be noisy about, and lowering them would let new uncalled functions through, the #716 case. A metric that covers anything never gets a floor of 0: where the branch allowance would round to 0, the floor is 1, a bar that fails only when the tests stop reaching that metric at all. Nothing is excluded, and the thin files keep honest low floors. Floors don't ratchet: coverage going up needs nothing, and when the gap is a branch only a hollow test could reach, lowering the row in the same PR with the reason is the documented fix. CI's Linux coverage table matched the macOS one to the hundredth.

Without a build. Floors are judged only on a green run. Without bulma-ui/dist the suites fail exactly as before, with their own "build first" messages, and the reporter adds one line saying it didn't check.

How the gate fails the run. Node documents no way for a reporter to fail a run, so the reporter sets process.exitCode. The test sibling runs a real node --test against it in both directions, so a Node upgrade that stops honouring that turns the suite red.

Known limit. Node only reports files a test loads. gen-component-catalog.mjs (checked by gen:catalog:check) and exports-resolve-fuzz.mjs (a manual fuzz tool) are loaded by no test, so they aren't in the table, and a new script with no test at all won't be either.

It adds about eight seconds to the scripts suite locally.

pnpm all passes locally.

The root node:test suite ran with no coverage at all, so nothing noticed
when a script's tests stopped reaching part of it. Every jest package has a
threshold; these files had none.

`pnpm test:scripts` now runs the suite with Node's coverage on, limited to
scripts/, and prints the table through the spec reporter on every run. A
second reporter, scripts/coverage-floors.mjs, compares each file against its
row in scripts/coverage-floors.json and fails the run when a file drops below
its floor, when a loaded file has no row, or when a row names a file no test
loaded. `pnpm test` and `pnpm all` both call it, so CI's existing Test step
gates it with no workflow change.

Per file rather than Node's global --test-coverage-* flags because the total
is dominated by the largest files. check-conformance.mjs alone is a quarter
of the measured lines, so a small script can lose half its coverage and move
the total by about a point.

Floors are today's numbers rounded down to a whole percent, measured on Node
22, 24 and 25. They are a record, not a target: lowering one in the same
change is the documented fix when the gap is a branch only a hollow test
could reach.

Floors are judged only on a green run. Without bulma-ui/dist the suites fail
exactly as before, with their own "build first" messages, plus one line
saying the floors were not checked.

Closes #724
@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 55 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: allxsmith/bestax/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: d57921eb-6c2b-45e4-a151-167d317e89b8

📥 Commits

Reviewing files that changed from the base of the PR and between 62feab9 and 76e9a94.

📒 Files selected for processing (7)
  • CLAUDE.md
  • CONTRIBUTING.md
  • docs/docs/guides/getting-started/contributing.md
  • package.json
  • scripts/coverage-floors.json
  • scripts/coverage-floors.mjs
  • scripts/coverage-floors.test.mjs
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://484672c0.bestax.pages.dev

Comment thread scripts/coverage-floors.json Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 1 blocking · 5 advisory

N Severity Area Finding Location
1 🟠 Major Robustness Branch floor of 89 is cleared by one branch on a figure measured to swing by two (539↔541 of 605 across 7 runs); 538/605 = 88.93% reds the Test step on every open PR scripts/coverage-floors.json:33
2 🔵 Advisory Coverage A script no test loads is outside the gate entirely — gen-component-catalog.mjs and exports-resolve-fuzz.mjs today, and any new script tomorrow. The “loaded file with no row” rule structurally cannot see them. Disclosed in the PR body, but not where a contributor reads the gate scripts/coverage-floors.mjs:13
3 🔵 Advisory Correctness PR body: “counts one branch more or less from run to run, which stays inside its floor” — the measured swing is two branches, and the low state clears the floor by one. Maintainer-facing; the body is not editable from a review PR body
4 🔵 Advisory Robustness A floor of 0 for functions is an inert row: file[field] < 0 is never true, so that metric is switched off for this file permanently rather than recording a low bar scripts/coverage-floors.json:9
5 🔵 Advisory Coverage The default fail runs only in the unmeasured nested process, so the one line that actually reds CI is the one line this file’s own row records as uncovered. Documented and accepted, but the safety net for it is the e2e pair, not the floor scripts/coverage-floors.mjs:127
6 🔵 Advisory Robustness .github/CLAUDE.md:339 still describes root pnpm test as running node --test "scripts/*.test.mjs" — still true, now by way of test:scripts. .github/** is human-authored, so this is not the fix agent’s to take .github/CLAUDE.md:339

Overall: The design is right, and the reasoning in the module comment is unusually good — per-file rather than a total is the correct read of #724, “judge only a green run” is the correct read of the no-build case, and the e2e pair that drives a real node --test in both directions is the correct answer to a reporter that can only fail a run by side effect. The riskiest part is not the code but the data file: the floors were measured on macOS and rounded down to a whole percent, which hands each row somewhere between 0.00 and 1.00pp of slack depending on where its number happened to land, and seven runs here show one metric moving further than its slack allows. Look at finding 1 first — it is a one-character edit, and the cost of skipping it is a red Test step on PRs that never touched scripts/.

Residual risk:

  • macOS floors vs. the first Linux measurement — refuted for 35 of 36 rows. pnpm test:scripts is green here on Linux / Node 24.21.0 after pnpm run build; all 36 rows report at or above floor, 1037 tests pass. Repeated 7 times, and only render-triage-comment.mjs moved (finding 1).
  • Checkout without bulma-ui/dist — refuted. Run before building, the suites failed on their own pre-existing bulma-ui/dist is absent / eslint-plugin/dist is absent assertions, the reporter printed exactly [coverage-floors] Not checked: tests failed, … and piled on nothing, and the run still exited 1.
  • A conditionally-skipped test making a live row read as “no test loaded” — refuted. No skip option, .skip, or platform gate exists in any scripts/*.test.mjs; the only process.env use is a set/restore inside render-triage-comment.test.mjs.
  • --test-coverage-exclude replacing Node’s default node_modules exclusion — refuted. The include glob is anchored at scripts/**/*.mjs and there is no scripts/node_modules, so nothing leaks into the table.
  • Prettier expanding the compact JSON rows and breaking the paste-in message — refuted. format:check globs only ts/tsx/js/jsx/mjs/md/mdx; .json is not in it. pnpm format:check and eslint on the three new files are both clean. (A bare prettier --check on the JSON does want to expand it — a trap for anyone who later widens that glob, not a failure today.)
  • pnpm run check:conformance fails here only on version-regression, from this sandbox’s shallow clone — unrelated to the diff. fragile-prose and turbo-tasks, the two rules the new prose and the new all segment could have tripped, both pass.
  • Open: the failing-floor path is verified only through the fixture. The PR’s e2e case proves the exit code with a real node --test, but with one reporter; the shipped command runs two on the same destination. I could not re-drive that combination in this sandbox. Low risk — the reporter yields only after draining the stream, and its success line does land last in the real run — but it is unchecked.
  • Open: a test added under scripts/lib/ never runs. The run glob is scripts/*.test.mjs while the new coverage exclude is the broader scripts/**/*.test.mjs, so such a file would be silently un-run and silently un-measured. Pre-existing, but the asymmetry between the two globs now invites it.
  • Open: the include glob is *.mjs only. A scripts/foo.js or .ts sits outside the gate with no complaint. None exists today.

🏄 Solid set, dude — this one actually reads the wave instead of guessing at it: per-file floors, a red run left un-judged, and a real node --test keeping the exit code honest. Only thing is that one floor parked a single branch above the tide line, and I watched the tide move twice that far. Drop it a point and paddle out. 🤙

Review of #841 found render-triage-comment.mjs's branch floor of 89 cleared
by less than one branch. The noise behind that is V8 counting the never-run
`?? 'unknown'` fallback in its execution-file error path in some runs and not
in others, so the file measures 246 of 275 or 246 of 276. Rounding down to a
whole percent leaves less than one unit of margin on any count under a
hundred, so most rows had the same problem.

A row is now the percentage the file would measure with two fewer covered
lines, branches or functions than its worst run, rounded down. That survives
two units of noise either way, which the test sibling checks over every count
up to 600 rather than on examples. A metric at 100% keeps a floor of 100: it
has no never-run code for the noise to come from, and a new function nothing
calls is what the gate is for. The row a failure message offers for a new
file uses the same rule.

The header also says what the gate cannot see (a script no test loads at all)
and that a row of 0 is no bar yet rather than a bar.
@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: fresh

73c543b replaces the hand-set floors with a rule and regenerates the table. Review the gate as it stands now.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://b8dc88f2.bestax.pages.dev

Comment thread scripts/coverage-floors.mjs Outdated
The two-unit allowance went on all three metrics, but the only noise ever
measured is V8 counting a never-run branch in some runs and not others.
Lines and functions do not move between runs, and on small files the
allowance cost the gate its point: fetch-retry.mjs functions dropped to a
floor of 50, so four new functions nothing calls would pass, and
gen-skills-rosters.mjs functions reached 0, which can never fail.

The allowance is now per metric, two on branches and none on lines or
functions, which go back to their rounded-down measured value. Branch rows
keep their numbers.

A covered metric also no longer gets a floor of 0 from the allowance. It gets
1, the smallest bar there is, which fails only if the tests stop reaching
that metric, and only when two more never-run branches still clear it. A row
of 0 now means the file measured 0. The row a failure message offers for a new
file follows the same rule, and the property case sweeps branches only.
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://252668b0.bestax.pages.dev

Comment thread scripts/coverage-floors.mjs
Comment thread package.json

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review — 2 blocking · 5 advisory

# Severity Area Finding Location
1 🟡 Minor Robustness The floor side is guarded against a missing number, the measured side is not: a renamed coveredBranchPercent makes every metric pass while the reporter prints success scripts/coverage-floors.mjs:159
2 🟡 Minor Robustness Nothing reads the gate back — dropping --test-reporter=./scripts/coverage-floors.mjs removes it silently, while dropping --experimental-test-coverage is caught package.json:10
3 🔵 Advisory Robustness The noise mechanism the header and the PR body describe is not the one this platform shows: on Linux / Node 24.21.0 the drift is ±2 covered branches (541↔539 of 605), not ±1 on the total, so "The branch noise moves a count by one; two leaves a unit to spare beyond it" understates it by the whole allowance. Deleting that clause leaves the rest true — and the rule it justifies is the correctly shaped one for covered-side drift, which is the lucky part. scripts/coverage-floors.mjs:90
4 🔵 Advisory Coverage A uniform two-covered-unit allowance erases the branch bar where the denominator is tiny: gen-api-docs.mjs 33.33 → floor 1, gen-skills-rosters.mjs 60.00 → floor 20, so losing 2 of 5 covered branches passes. Same shape as the functions case already fixed, now confined to two rows. scripts/coverage-floors.json:8
5 🔵 Advisory Robustness Every lines and functions floor is exactly Math.floor of a Node 24 run, so those metrics carry zero margin, and several sit under one unit: check-conformance lines 70.11 vs 70 is about 3 lines, auto-close-duplicates 50.19 vs 50 and consumer-sbom-meta 98.16 vs 98 are both under one line. Deliberate, but it makes pnpm test version-sensitive for a contributor on Node 22 (what .nvmrc pins), which I could not test — only 24 is installed here. scripts/coverage-floors.json:1
6 🔵 Advisory Coverage gen-api-docs.mjs functions is 0 of 12, an inert row by construction, and its lines floor is 17 — the header is honest about this, but it means the largest uncovered script is gated on one metric of three. scripts/coverage-floors.json:9
7 🔵 Advisory Correctness docs/docs/guides/getting-started/contributing.md:64 already tells readers what pnpm test covers and was not updated alongside CONTRIBUTING.md; nothing syncs the two. docs/docs/guides/getting-started/contributing.md:64

Overall: The gate is sound and the regeneration is faithful — I re-derived all 36 rows from four live pnpm test:scripts runs and every one is exactly what the rule produces (lines and functions are Math.floor of the measured percentage, branches are floor(100 * (covered - 2) / total) wherever the counts are recoverable), so this is a rule applied mechanically rather than a hand edit. The earlier blocking row is genuinely fixed and with room: the drift is real and bistable (render-triage-comment.mjs branch alternates 89.42 and 89.09 and nothing else moves), and floor 88 fails only below 532 of 605 — seven covered branches below the worse state. The riskiest part is not the numbers but that nothing outside this module asserts the module is still wired in, so read finding 2 first and the asymmetric guard in finding 1 second. I also settled the one thread the previous pass left open (the per-metric allowance): verified against the current code and resolved.

Residual risk:

  • Drift larger than the allowance — refuted for the platform CI runs on. Four runs, bistable 89.42 / 89.09 with every other row byte-identical, and floor 88 tolerates seven lost covered branches from the worse state rather than the two the allowance names.
  • Rows at 100 that are fed by spawned children — open. handoff-message.mjs reaches 100 on all three metrics partly through spawnSync(process.execPath, [SCRIPT, ...]) children, whose coverage merges only because they inherit NODE_V8_COVERAGE; sanitize-repro-draft.mjs and parse-scan-verdict.mjs have the same shape. A metric at 100 keeps a floor of 100 by design, so one missed merge reds every PR with a message telling the reader to cover lines their tests already cover. Stable across four runs and loud rather than silent, so not a hole — but it is also the likeliest source of the covered-side drift in finding 3, and unlike a V8 enumeration quirk its magnitude is not pinned by anything measured here.
  • A script no test loads stays outside the gate — confirmed and documented. gen-component-catalog.mjs and exports-resolve-fuzz.mjs appear in neither the coverage table nor any test import (the single mention is a comment in publishable-manifests.test.mjs); gen:catalog:check still covers the first, nothing covers the second.
  • A red run is not judged — by design, so a change that both breaks a test and drops coverage is measured only once the test is fixed. The end-to-end case pins the behaviour, and the exit code is 1 either way.
  • A lost test sibling taking a script out of the gate — refuted. The row-with-no-loader rule fails loudly, is covered by a unit case, and the real run shows 36 measured files against 36 rows.
  • Floors never ratchet — stated and intended, so a file whose coverage climbs and then falls back is only held to where it stood when its row was written. Nothing in CI notices a row that has drifted far below its file.

🏄 Fourth set on this wave and it lines up clean, dude — the rule does the work and the table is just the rule wearing a jacket. I watched that gnarly render-triage branch bob up and down four times and it never dipped under the line. Only thing left is that nobody is watching the leash tying the gate to pnpm test, so tie that off and paddle out.

…ired in

The floor side was guarded against a missing number and the measured side
was not. A field Node renamed in its coverage summary compares false against
every floor, so every file would pass and the reporter would print success.
Every figure the reporter reads, percentages and counts alike, must now be a
real number or that file fails by name. Checking the counts too stops a new
file being offered a made-up row of zeros.

Nothing read the wiring back either. Dropping the reporter flag from
test:scripts left the suite green with no gate. The test sibling now reads
the root package.json and fails if test:scripts stops turning coverage on or
loading the reporter, or if `test` or `all` stops running it.

The header no longer claims the branch noise moves a count by one. It moves
in both how many branches V8 counts and how many it counts as covered, and
the allowance covers both.
@allxsmith

Copy link
Copy Markdown
Owner Author

On the advisories:

  • 3 (the noise clause). Deleted. The header now says V8's branch counts move in both how many branches it counts and how many it counts as covered, which covers both drift shapes, and the property test checks both.
  • 4 (tiny branch denominators). On the record. It's confined to two rows, and they're the least-tested files in scripts/.
  • 5 (lines and functions at the measured value). Kept. Lines and functions matched to the hundredth on Node 22, 24 and 25 across repeated runs, and CI's Linux / Node 24 table matched them too. The only figure that ever moved was render-triage-comment.mjs branches. So a contributor on Node 22, which .nvmrc pins, sees the same line and function numbers as CI.
  • 6 (gen-api-docs.mjs functions at 0 of 12). On the record. The header says what a row of 0 means.
  • 7 (the docs contributing guide). Fixed in c969f5a. Both contributing guides now list pnpm run test:scripts among the individual checks.

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://eceac500.bestax.pages.dev

@allxsmith

Copy link
Copy Markdown
Owner Author

deep-review: verify the two open threads against bcb1c81 and c969f5a. Fresh runs on this PR keep reaching the time limit, and the last full review completed at 11:26.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep review (verify) — 2 resolved · 0 open

# Severity Area Finding Location Status
1 🟡 Minor Robustness The floor side was guarded against a missing number; the measured side was not, so a renamed Node summary field would have read as a pass on every file at once scripts/coverage-floors.mjs:178 ✅ Verified fixed in bcb1c81
2 🟡 Minor Robustness Nothing read the wiring back: dropping the reporter flag from test:scripts left the suite green with no gate package.json:10 ✅ Verified fixed in bcb1c81

What I checked

1 — the measured-side guard. checkFloors now screens every figure it reads before any comparison: the flatMap over METRICS yields all nine summary fields (three percentages plus six counts), and !Number.isFinite catches both a renamed field (undefined) and a NaN, so the file is reported by name and the loop continues rather than reaching file[field] < floor[name] — which was false on either operand. Widening it to the counts is load-bearing rather than incidental: the missing-row branch calls pct(file[field]) and measuredRow reads the counts, so a renamed count would otherwise have offered a new file a paste-in row of zeros. The guard sits ahead of the floors[key] lookup while measured.add(key) stays behind it, so an unusable file yields one problem instead of also tripping the orphan-row rule. The end-to-end "floors that hold" case does pin this against Node real coverage event as the author states: it asserts an exit status of 0, which a rename would break via fail().

2 — the wiring assertion. The new sibling case reads the root manifest and splits test:scripts with the repo own sh tokenizer (scripts/lib/shell-words.mjs), so the quoted globs do not confuse the word split. The coverage assertion is a word-level includes rather than a substring; the reporter assertion collects both the --test-reporter=X and two-word --test-reporter X spellings and compares resolve(REPO, path) against the absolute module path, so a respelled path still matches while a dropped flag leaves only spec and fails with the loaded reporters named.

Empirically: full pnpm build then pnpm test:scripts on this checkout (Linux, Node 24.21.0) — 1043 pass, 0 fail, all three new cases green, and the reporter prints "Every measured file (36) is at or above its floor", confirming the widened nine-field guard does not false-positive on real Node output.

Overall: This pass settled the two threads the earlier review left open and reviewed no commits — it is not a fresh review of bcb1c81 or c969f5a, and it raises no new findings. Both fixes are real, and each lands slightly wider than what was asked for in ways that hold up under reading and under the suite. The riskiest part of the change remains the floor table itself rather than this round edits, and it is green end to end on a fully built checkout.

Residual risk:

  • Renamed summary fields — refuted for the silent-pass shape: all nine fields are screened, and the end-to-end case reads Node real event, so a rename fails the run rather than quietly passing it.
  • The gate being unwired — the reporter-flag half, the edit most likely to happen, fails loudly. The test / all half is self-referential (a change dropping test:scripts from both entry points also stops the case running); recorded here rather than reopened, since it is exactly the assertion the original finding asked for.
  • Include and exclude globs unpinned — refuted: a widened include surfaces as files with no row, a narrowed one as rows no test loaded, and Node excludes test files from coverage by default.

🏄 Two clean cutbacks on the last two open threads — the author paddled back out and patched both wider than the wave asked for, the suite came in 1043-for-1043, and the gate is holding its line. Nothing left in the water on this one.

@allxsmith allxsmith added the needs-human-review Loop converged (or contested): awaiting owner review + manual merge label Sep 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📸 Story screenshots at handoff — c969f5a

No Storybook stories map to this PR's changed files — nothing to screenshot. (workflow run)

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://71136025.bestax.pages.dev

@allxsmith
allxsmith merged commit ddff650 into main Oct 1, 2026
13 checks passed
@allxsmith
allxsmith deleted the test/724-script-coverage branch October 1, 2026 03:04
@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 5.19.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.14.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 2.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 4.2.13 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deep-review needs-human-review Loop converged (or contested): awaiting owner review + manual merge released

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nothing gates coverage on scripts/*.test.mjs

1 participant