Skip to content

ci(frontend-next): measure real coverage and ratchet it so it cannot fall (#3318) - #3438

Closed
Xore wants to merge 1 commit into
mainfrom
oc/3318-coverage
Closed

Xore wants to merge 1 commit into
mainfrom
oc/3318-coverage

Conversation

@Xore

@Xore Xore commented Sep 27, 2026

Copy link
Copy Markdown
Owner

What

Closes the measurement half of #3318 and adds the ratchet. frontend-next
ran vitest and Playwright in quality.yml with nothing collecting coverage,
so there was no baseline and nothing that could tell a change which deleted
tested behavior from one which did not.

  • @vitest/coverage-v8 + npm run test:coverage: v8 line and branch
    coverage over src/ only, all: true so an untested file reports 0%
    rather than nothing, reporters text / json-summary / html, written to
    coverage/ (gitignored, published as a CI artifact).
  • scripts/check-frontend-next-coverage.py: reads the summary that run just
    produced and compares it to the committed coverage-baseline.json, so the
    gate cannot be satisfied by a hardcoded constant. It also fails on a
    *.test.ts that vitest's own include: globs would never collect.
  • Three steps in the existing frontend-next / frontend-next-cloud pair:
    measure, gate, publish. Nothing re-installs a toolchain — the node runtime
    is the one ci: frontend-next is tested on Node 24 but the image runs Node 22 — test on the runtime major #3331 derives from the image, and npm ci is already there.

The measured baseline

Run on the image's own runtime — node:22-alpine@sha256:c610fcdfb1d5, node
22.23.2 / npm 10.9.8, the FROM line #3331 exists to keep CI on — with the
lockfile installed by npm ci:

$ npm run test:coverage
 Test Files  24 passed (24)
      Tests  215 passed (215)
All files          |   10.16 |     4.77 |    4.01 |   10.95
                   | % Stmts | % Branch | % Funcs | % Lines

10.95% lines (806 of 7359) and 4.77% branches (346 of 7250).

That is low, and the reason is structural rather than mysterious:
src/routes/ is 23,450 lines of route module with almost no unit tests
behind it. It is recorded as measured. Nothing was excluded to raise it —
the one exclusion in the config is the generated src/routeTree.gen.ts, and
that one costs coverage rather than buying it: including it reports
11.63% lines. Measured, not assumed; the config comment says so.

The All files line above is reproduced by a clean npm ci from the
committed lockfile, and the runner was verified bit-identical across three
runs of the same tree (v8 coverage of an unchanged tree is deterministic), so
the tolerances are not absorbing measurement noise.

The ratchet, and how it was shown failing

Two conditions on the same measurement, because a percentage over 7,359 lines
cannot see a small regression:

  1. the percentage may not fall more than tolerancePoints (0.5) below the
    baseline — the issue's own ask;
  2. the covered line and branch counts may not fall at all.

Condition 2 is what makes condition 1 worth having, and that is a measured
claim, not a preference:

probe real run gate
unchanged tree 10.95% / 4.77% GREEN (exit 0)
+7-line untested module 10.94% lines, 806 covered GREEN — and that is the finding: a percentage gate with any tolerance at all misses this. The covered count is what sees it, and 806 is unchanged, because adding untested code is not a deletion of tested behavior. The count condition earns its keep on the other direction.
+400-line untested module 10.38% lines, 4.61→3.90% branches, all 215 tests still passing RED (exit 1) — coverage lines dropped, coverage branches dropped
summary with 40 covered lines + 12 covered branches removed 10.41% / 4.61% RED — covered lines regressed, covered branches regressed
baseline raised to 99% — RED — both metrics
measured summary missing — RED (exit 2, fails closed)
a *.test.ts outside the include globs — RED — discovery guard

Each RED is the real script against real inputs; the only thing that varies
is which measured/baseline file it reads, or the presence of one temporary
probe file that was removed immediately after. The 400-line probe was a
generated throwaway src/lib/zzRatchetProbe.ts, deleted before the commit —
no existing test file was touched to produce any of those runs.

The gate reads the measured value, not a constant: --measured is how the
RED runs above were produced, and the CI step passes no arguments at all, so
in CI both the measured path and the threshold are committed files.

Locking the gate

tests/docs/test_3318_frontend_coverage_ratchet.py (39 tests, in the
existing tests/docs/ pytest row — no new CI wiring needed) asserts:

  • the decision logic against fixtures, including the seven-line case above
    and the per-metric reporting that stops branches-for-lines trades hiding
    inside an average;
  • the **-glob translation, including the zero-directory case fnmatch gets
    wrong — the guard's one catastrophic failure mode is matching too little;
  • the exit codes end to end through the real command line, including that a
    missing or unreadable summary is 2 and never a pass;
  • that the CLI cannot grow a --tolerance / --update flag, so the committed
    baseline is the only place strictness can change;
  • that the baseline's percentages follow from its own counts, and that its
    tolerances stay small (0 < tolerancePoints <= 1, toleranceCounts == 0);
  • the CI wiring: both twins, measure-before-gate, no arguments to the gate,
    if-no-files-found: error, the two jobs' steps still identical, the plain
    npm test / typecheck / build / route-tree steps still present, and no new
    action introduced unpinned.

Deliberately not done

  • No test weakened, skipped, deleted or excluded from the measurement.
    npm test, typecheck, build and the generated-route-tree diff all
    still run in both twins.
  • No threshold lowered and no file excluded to raise the number. The one
    exclusion is generated code, and it lowers the figure.
  • No new GitHub action. The report upload reuses the SHA the job already
    pinned (upload-artifact@ea165f8… v4.6.2), and zizmor + actionlint both
    run clean on the changed workflow.
  • openapi.json untouched — nothing here reaches the Rust crate.
  • The lockfile was regenerated with npm 10 from the image, not npm 11.
    A local npm install under npm 11 rewrote unrelated ioredis dedupe
    entries; that churn was reverted and the lockfile regenerated inside
    node:22-alpine, so the diff is 171 pure insertions and the Dependabot lockfile bumps break npm ci for every following PR — check it in the Dependabot PR #1816
    "lockfile installs under the image's npm" step keeps meaning something.
  • No test was added for the frontend's own coverage gaps. 23,450 lines of
    route module at ~0% is the real finding here; raising it is follow-up work,
    not something to smuggle into a measurement change.
  • The tolerancePoints value is 0.5, not 0. A v8 run is deterministic,
    so zero would work mechanically, but a PR adding one ordinary untested
    function would then fail CI for it. The pressure valve the issue asks for
    is the explicit baseline commit, and it is the only one.

Refs #3318

…fall (#3318)

frontend-next ran vitest and Playwright in quality.yml with nothing
collecting coverage, so there was no baseline and no way to tell a change
that deleted tested behavior from one that did not. Add @vitest/coverage-v8
and a `test:coverage` script (v8, lines and branches, over src/, published
as an artifact), then hold the number with a gate.

Measured, on the image's own runtime (node 22, node:22-alpine@sha256:c610fcd):

    npm run test:coverage
    Test Files  24 passed (24)
         Tests  215 passed (215)
    All files          | % Stmts | % Branch | % Funcs | % Lines
    All files          |   10.16 |     4.77 |    4.01 |   10.95

10.95% lines, 4.77% branches, over 7359 lines and 7250 branches. Low because
src/routes/ is 23,450 lines of route module with almost no unit tests behind
it -- recorded as measured rather than trimmed to look better. The one
exclusion in the coverage config is the generated src/routeTree.gen.ts, and
it costs coverage rather than buying it: including it reports 11.63%. That
was measured, not assumed, and the config comment says so.

scripts/check-frontend-next-coverage.py reads the summary that run just
produced and compares it to the committed coverage-baseline.json, so the
gate cannot be satisfied by a constant. Two conditions on the same
measurement:

  - the percentage may not fall more than tolerancePoints (0.5) below the
    baseline, and
  - the covered line and branch counts may not fall at all.

The second is what makes the first worth having, and that is a measured
claim rather than a preference. Over 7359 lines one new untested seven-line
module moved the line percentage 10.95% -> 10.94%, which any percentage
tolerance passes -- so the percentage alone would not have noticed a real
regression. A 400-line untested module moves it to 10.38% and the gate goes
red with all 215 tests still green, which is the gap this issue describes.
The same script fails when a *.test.ts exists that vitest's own `include:`
globs would never collect: a test file nothing runs is a file, not a check,
and it would otherwise sit inside the number while contributing nothing.

The baseline moves only by editing it in a commit. There is no --update
flag, no tolerance on the command line, and the CI step passes no arguments
at all, so the measured path and the threshold are both committed files. A
missing or unreadable summary exits 2 rather than passing.

tests/docs/test_3318_frontend_coverage_ratchet.py (39 tests) pins the gate
itself: the decision logic against fixtures, including the seven-line case
that the percentage tolerance cannot see; the glob translation, including
the `**`-matches-zero-directories case fnmatch gets wrong; the end-to-end
exit codes through the real command line; that the CLI cannot grow a flag
that widens the gate; that the baseline's own percentages follow from its
own counts; and the CI wiring, the untouched `npm test` step, and that no new
action was introduced unpinned.

The three new steps go into the existing frontend-next/-cloud pair, which
already has the node runtime #3331 derives from the image and a `npm ci`, so
nothing re-installs a toolchain. The report upload reuses the pinned
upload-artifact SHA the job already uses, with if-no-files-found: error so
"the report was published" is a fact the step can fail on.

No existing test was weakened, skipped or deleted, no file was excluded to
raise the number, no threshold was lowered, and openapi.json is untouched.
@github-actions

Copy link
Copy Markdown

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses.
  • ⚠️ 1 packages with OpenSSF Scorecard issues.
See the Details below.

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
npm/@bcoe/v8-coverage 1.0.2 UnknownUnknown
npm/@vitest/coverage-v8 4.1.11 UnknownUnknown
npm/ast-v8-to-istanbul 1.0.7 UnknownUnknown
npm/has-flag 4.0.0 🟢 3.5
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Maintained⚠️ 01 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Code-Review⚠️ 1Found 4/23 approved changesets -- score normalized to 1
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Security-Policy🟢 9security policy file detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/html-escaper 2.0.2 ⚠️ 2
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow⚠️ -1no workflows found
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review⚠️ 0Found 0/27 approved changesets -- score normalized to 0
Token-Permissions⚠️ -1No tokens found
Packaging⚠️ -1packaging workflow not detected
Pinned-Dependencies⚠️ -1no dependencies found
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/istanbul-lib-coverage 3.2.2 🟢 4
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 5Found 11/20 approved changesets -- score normalized to 5
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License⚠️ 0license file not detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/istanbul-lib-report 3.0.1 🟢 4
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 5Found 11/20 approved changesets -- score normalized to 5
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License⚠️ 0license file not detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/istanbul-reports 3.2.0 🟢 4
Details
CheckScoreReason
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 5Found 11/20 approved changesets -- score normalized to 5
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
License⚠️ 0license file not detected
Fuzzing⚠️ 0project is not fuzzed
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 10security policy file detected
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/magicast 0.5.5 UnknownUnknown
npm/make-dir 4.0.0 🟢 3.7
Details
CheckScoreReason
Code-Review🟢 3Found 9/30 approved changesets -- score normalized to 3
Binary-Artifacts🟢 10no binaries found in the repo
Maintained⚠️ 01 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Security-Policy🟢 9security policy file detected
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/supports-color 7.2.0 🟢 4.1
Details
CheckScoreReason
Maintained🟢 44 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 4
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 3Found 11/30 approved changesets -- score normalized to 3
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Security-Policy🟢 9security policy file detected
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • arcane/home/honeypot-dashboard/frontend-next/package-lock.json

@Xore
Xore enabled auto-merge (squash) September 27, 2026 22:52
@Xore
Xore disabled auto-merge September 27, 2026 23:22
@Xore

Xore commented Sep 27, 2026

Copy link
Copy Markdown
Owner Author

Closing as superseded.

All of this PR's content landed via #3417 (squash e259bf0b): the coverage ratchet job in quality.yml, scripts/coverage-ratchet.mjs, and the measured 10.95% lines / 4.77% branches baseline are all present on main.

It shared branch oc/3318-coverage-ratchet with #3417, so it could not land independently. Replaying its only remaining commit (bb912657) onto current main yields an empty cherry-pick — nothing to commit. The only diff the branch still carries is a stale scripts/compose-drift-watch.py that would revert #3440's fix (is_regular_file()), re-breaking the dead except PermissionError handlers.

Keeping it open would only re-introduce the CONFLICTING state.

@Xore Xore closed this Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant