From c9d2f35ec172589a4236a7ebc69ae5a01f7aa121 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Wed, 21 Oct 2026 10:42:02 -0700 Subject: [PATCH] ci(coverage): fast check that every frontend file has a coverage baseline The frontend coverage job only reports a missing baseline entry after its full browser run. coverage:baseline-check compares the files the gate measures (same discoverSources) with frontend-coverage-baseline.json in milliseconds and also flags entries for files that no longer exist. npm test runs the same check. --- TESTING.md | 7 ++++ tests/vendor/coverage-baseline-check.test.mjs | 33 ++++++++++++++++ tools/vendor/coverage-baseline-check.mjs | 38 +++++++++++++++++++ tools/vendor/package.json | 1 + 4 files changed, 79 insertions(+) create mode 100644 tests/vendor/coverage-baseline-check.test.mjs create mode 100644 tools/vendor/coverage-baseline-check.mjs diff --git a/TESTING.md b/TESTING.md index 98a075a..147eb95 100644 --- a/TESTING.md +++ b/TESTING.md @@ -109,6 +109,13 @@ the baseline updater. Raise floors with `npm run --prefix tools/vendor coverage:update`; it reruns the suites and cannot lower an existing floor. Review baseline changes like code changes. +`npm run --prefix tools/vendor coverage:baseline-check` runs in well under a +second without a browser and fails, naming the files, when a measured +`web/js` file has no baseline entry or the baseline lists a file that no longer +exists. It uses the gate's own `discoverSources`, and `npm test` runs the same +check, so run it before pushing a new or deleted frontend file instead of +waiting for the `frontend coverage` job. + The `viewerjs` check runs on every PR, including backend-only changes. It is an aggregate job that passes only when the `frontend coverage (chromium and touch)` job and both `page workflows (firefox)` and `page workflows (webkit)` jobs diff --git a/tests/vendor/coverage-baseline-check.test.mjs b/tests/vendor/coverage-baseline-check.test.mjs new file mode 100644 index 0000000..187d174 --- /dev/null +++ b/tests/vendor/coverage-baseline-check.test.mjs @@ -0,0 +1,33 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { baselineGaps, measuredSources, readBaseline } from '../../tools/vendor/coverage-baseline-check.mjs'; + +const floor = { lines: 80, branches: 80, functions: 80, statements: 80 }; + +test('baseline gaps name measured files without a baseline entry', () => { + const gaps = baselineGaps(['a.js', 'nested/new.mjs'], { 'a.js': floor }); + assert.deepEqual(gaps.missing, ['nested/new.mjs']); + assert.deepEqual(gaps.stale, []); +}); + +test('baseline gaps name baseline entries whose file no longer exists', () => { + const gaps = baselineGaps(['a.js'], { 'a.js': floor, 'gone.js': floor }); + assert.deepEqual(gaps.missing, []); + assert.deepEqual(gaps.stale, ['gone.js']); +}); + +test('baseline gaps are empty when files and baseline agree', () => { + assert.deepEqual(baselineGaps(['a.js'], { 'a.js': floor }), { missing: [], stale: [] }); +}); + +test('the committed baseline covers exactly the measured frontend sources', () => { + const gaps = baselineGaps(measuredSources(), readBaseline()); + assert.deepEqual(gaps.missing, [], `Missing baseline (run coverage:update): ${gaps.missing.join(', ')}`); + assert.deepEqual(gaps.stale, [], `Stale baseline entries (file removed): ${gaps.stale.join(', ')}`); +}); + +test('the real gate and this check select files through the same function', () => { + const gate = readFileSync(new URL('../../tools/vendor/frontend-coverage.mjs', import.meta.url), 'utf8'); + assert.match(gate, /discoverSources\(sourceRoot\)/); +}); diff --git a/tools/vendor/coverage-baseline-check.mjs b/tools/vendor/coverage-baseline-check.mjs new file mode 100644 index 0000000..795ccaf --- /dev/null +++ b/tools/vendor/coverage-baseline-check.mjs @@ -0,0 +1,38 @@ +// Fast, browser-free check that frontend-coverage-baseline.json lists exactly the +// files the coverage gate measures. CI's `frontend coverage` job reports a missing +// entry only after ~20 minutes; this reports it in milliseconds. +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { repositoryRoot } from './vendor-viewerjs.mjs'; +import { discoverSources } from './frontend-coverage.mjs'; + +const baselinePath = join(dirname(fileURLToPath(import.meta.url)), 'frontend-coverage-baseline.json'); + +export function measuredSources() { + return discoverSources(join(repositoryRoot, 'web/js')); +} + +export function readBaseline() { + return JSON.parse(readFileSync(baselinePath, 'utf8')); +} + +export function baselineGaps(files, baseline) { + const measured = new Set(files); + return { + missing: files.filter(file => !baseline[file]), + stale: Object.keys(baseline).filter(file => !measured.has(file)).sort(), + }; +} + +function main() { + const { missing, stale } = baselineGaps(measuredSources(), readBaseline()); + if (!missing.length && !stale.length) { + console.log('Coverage baseline matches the measured frontend files.'); + return; + } + if (missing.length) console.error(`Missing baseline for:\n ${missing.join('\n ')}\nRun: npm run --prefix tools/vendor coverage:update`); + if (stale.length) console.error(`Baseline lists files that no longer exist:\n ${stale.join('\n ')}\nRemove them from tools/vendor/frontend-coverage-baseline.json`); + process.exitCode = 1; +} +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/tools/vendor/package.json b/tools/vendor/package.json index 0fda145..c64b903 100644 --- a/tools/vendor/package.json +++ b/tools/vendor/package.json @@ -15,6 +15,7 @@ "test:comfyui": "playwright test --config comfyui.config.mjs", "test:executions": "playwright test --config playwright.execution.config.mjs", "test:coverage": "node frontend-coverage.mjs", + "coverage:baseline-check": "node coverage-baseline-check.mjs", "coverage:update": "node frontend-coverage.mjs --update-baseline", "test:compat": "playwright test --config pack-compat.config.mjs" },