From bd267c67f8e841df7e8089e8b5a3847cb0b235cd Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 8 Oct 2026 13:56:19 +0200 Subject: [PATCH] ci(client): fail the count ratchets on an entry above the tree lint:size, lint:layers, lint:rtl (both baselines), lint:skips and lint:warnings only failed a count that passed its entry. A count that fell below its entry was logged and nothing more, so a file that shrank could grow back to its old entry unseen, and the entry of a deleted file stayed behind for the next file at that path. lint:format and lint:offline already fail on such a stale entry, and the docs said these counts may only shrink. Each of them now fails on an entry that lowerCounts would lower or drop, naming its count or saying the file is gone, and tells to run --update, which still only lowers. staleCounts and reportStale in lib/ratchet.mjs hold the shared part. Every baseline matches the tree today, so none needed lowering. The client CLAUDE.md says so, and that CI lints through lint:warnings rather than lint:check. --- client/CLAUDE.md | 6 ++- client/scripts/eslint-ratchet.mjs | 4 +- client/scripts/layer-lint.mjs | 3 +- client/scripts/lib/eslint-warnings.mjs | 26 ++++++++--- client/scripts/lib/layers.mjs | 10 ++--- client/scripts/lib/ratchet.mjs | 44 +++++++++++++++++++ client/scripts/lib/rtl.mjs | 14 +++--- client/scripts/lib/size.mjs | 14 +++--- client/scripts/lib/skips.mjs | 9 ++-- client/scripts/rtl-lint.mjs | 4 +- client/scripts/size-lint.mjs | 5 ++- client/scripts/skip-lint.mjs | 2 + .../tests/unit/scripts/eslint-ratchet.test.ts | 17 +++++-- client/tests/unit/scripts/layer-lint.test.ts | 11 +++++ client/tests/unit/scripts/ratchet.test.ts | 32 ++++++++++++++ client/tests/unit/scripts/rtl-lint.test.ts | 18 ++++++++ client/tests/unit/scripts/size-lint.test.ts | 17 +++++++ client/tests/unit/scripts/skip-lint.test.ts | 11 +++++ 18 files changed, 210 insertions(+), 37 deletions(-) diff --git a/client/CLAUDE.md b/client/CLAUDE.md index a897f9478..811308d52 100644 --- a/client/CLAUDE.md +++ b/client/CLAUDE.md @@ -8,8 +8,8 @@ Scope: the **`@trek/client`** workspace (React 19 + Vite + Zustand + Tailwind PW npm run dev # Vite dev server; proxies the API/ws/uploads/MCP/OAuth paths → http://localhost:3001 npm run build # prebuild generates PWA icons, then vite build npm run typecheck # tsc --noEmit (CI) -npm run lint # eslint . (lint:check in CI) -npm run lint:warnings # eslint, failing on any error and on any warning count per rule above scripts/eslint-baseline.json (app code, tests and eslint-disable'd messages counted apart; a `/* eslint : ... */` config comment fails outright; --update lowers it) +npm run lint # eslint . (CI runs lint:warnings instead, which fails on the same errors) +npm run lint:warnings # eslint, failing on any error and on any warning count per rule above scripts/eslint-baseline.json and on any entry above its count (app code, tests and eslint-disable'd messages counted apart; a `/* eslint : ... */` config comment fails outright; --update lowers it) npm run lint:pages # enforce the Page pattern (CI gate) npm run lint:rtl # physical left/right sides and rtl-lint-disable comments may only shrink per file (CI gate; --list shows them, --update lowers both baselines) npm run lint:size # no source file or stylesheet past 1000 lines, no test (Playwright specs and fixtures under e2e/ included) past 2000, counted wrapped at 120 columns; longer ones may only shrink (CI gate; --update lowers the baseline) @@ -25,6 +25,8 @@ npm run theme:lint # theme conformance audit (theme:lint:strict exits 1; npm run check:gl-split # after build: fails if one chunk bundles both mapbox-gl and maplibre-gl (CI gate) ``` +Every baseline ratchet above also fails on an entry that allows more than the tree holds: a count above what its file or rule has now, or a file that is gone. Run the check with `--update` in the change that made it smaller, so a fixed file cannot grow back to its old entry and a new file at a deleted path does not inherit one. + Single test: `npx vitest run src/store/slices/budgetSlice.test.ts`, or `npx vitest run -t "optimistically adds the place"`. The dev server needs the API on **:3001** — run `npm run dev` at the repo root to start both. diff --git a/client/scripts/eslint-ratchet.mjs b/client/scripts/eslint-ratchet.mjs index 472bdff46..b0b787714 100644 --- a/client/scripts/eslint-ratchet.mjs +++ b/client/scripts/eslint-ratchet.mjs @@ -8,7 +8,9 @@ * of the kind that has already shown stale money. This runs ESLint over the * client and counts the warnings per rule, separately for app code and * tests, and the messages an eslint-disable comment silenced as a third - * group. Each count may not pass its entry in scripts/eslint-baseline.json. + * group. Each count may not pass its entry in scripts/eslint-baseline.json, + * and an entry above its count fails as well until --update lowers it, so a + * fixed warning cannot come back unseen. * Any error fails as well, so the check can stand in for `npm run lint:check`, * and so does a block comment that configures a rule inline (such as one that * turns no-explicit-any off): it leaves no message to count, so it would diff --git a/client/scripts/layer-lint.mjs b/client/scripts/layer-lint.mjs index 6d16c256f..efc9a0f09 100644 --- a/client/scripts/layer-lint.mjs +++ b/client/scripts/layer-lint.mjs @@ -10,7 +10,8 @@ * all. The imports that already broke a rule are counted per file in * scripts/layers-baseline.json, and a file may hold no more of them than its * entry (a file without an entry holds none). Type-only imports count: a type - * the lower layer needs belongs below it. + * the lower layer needs belongs below it. An entry above what its file holds + * now, or for a file that is gone, fails as well until --update lowers it. * * npm run lint:layers check against the baseline (CI) * npm run lint:layers -- --list print every import against the layering diff --git a/client/scripts/lib/eslint-warnings.mjs b/client/scripts/lib/eslint-warnings.mjs index b72c243e1..7a5617df0 100644 --- a/client/scripts/lib/eslint-warnings.mjs +++ b/client/scripts/lib/eslint-warnings.mjs @@ -4,7 +4,17 @@ import { ESLint } from 'eslint'; import { join } from 'node:path'; import ts from 'typescript'; -import { countMap, lowerCounts, readBaseline, readText, TEST_FILE, toKey, writeBaseline } from './ratchet.mjs'; +import { + countMap, + lowerCounts, + readBaseline, + readText, + reportStale, + staleCounts, + TEST_FILE, + toKey, + writeBaseline, +} from './ratchet.mjs'; /** * Where a warning is counted: in the app code, in the tests, or, for a @@ -151,7 +161,7 @@ export async function check({ for (const line of errors) error(`FAIL ${line}`); let grown = 0; - let lowerable = 0; + let stale = 0; for (const area of AREAS) { for (const [rule, n] of Object.entries(counts[area])) { const allowed = baseline[area][rule] ?? 0; @@ -163,7 +173,14 @@ export async function check({ .map(([key, k]) => `${key} (${k})`); error(`FAIL ${area}: ${n} ${rule} warning(s), baseline ${allowed}. Most in: ${top.join(', ')}`); } - for (const [rule, allowed] of Object.entries(baseline[area])) if ((counts[area][rule] ?? 0) < allowed) lowerable++; + const fallen = staleCounts(baseline[area], counts[area]); + reportStale(fallen, { + file: 'eslint-baseline.json', + command: 'lint:warnings', + label: (rule) => `${area}: ${rule}`, + error, + }); + stale += fallen.length; } if (grown) { error( @@ -171,9 +188,8 @@ export async function check({ 'Run npx eslint to see each one.' ); } - if (lowerable && !update) log(`${lowerable} rule count(s) fell below the baseline: run with --update to lower it.`); log( `eslint: ${errors.length} error(s); warnings ${AREAS.map((area) => `${area} ${sum(counts[area])}/${sum(baseline[area])}`).join(', ')} (now/baseline)` ); - return errors.length || grown ? 1 : 0; + return errors.length || grown || stale ? 1 : 0; } diff --git a/client/scripts/lib/layers.mjs b/client/scripts/lib/layers.mjs index 22e5d6f1b..1b759f8c3 100644 --- a/client/scripts/lib/layers.mjs +++ b/client/scripts/lib/layers.mjs @@ -3,7 +3,7 @@ */ import { join } from 'node:path'; import { sourceGraph } from './imports.mjs'; -import { countMap, lowerCounts, readBaseline, writeBaseline } from './ratchet.mjs'; +import { countMap, lowerCounts, readBaseline, reportStale, staleCounts, writeBaseline } from './ratchet.mjs'; const VIEW = ['components', 'mobile', 'pages', 'hooks']; @@ -82,7 +82,7 @@ export function check({ } const grown = Object.entries(counts).filter(([key, n]) => n > (baseline[key] ?? 0)); - const lowerable = Object.entries(baseline).filter(([key, n]) => (counts[key] ?? 0) < n); + const stale = staleCounts(baseline, counts); for (const [key, n] of grown) { error(`FAIL ${key}: ${n} import(s) against the layering, baseline ${baseline[key] ?? 0}:`); @@ -94,12 +94,10 @@ export function check({ 'a hook under hooks/, a type under types/) instead of importing upwards.' ); } - if (lowerable.length && !update) { - log(`${lowerable.length} file(s) hold fewer of these imports than the baseline: run with --update to lower it.`); - } + reportStale(stale, { file: 'layers-baseline.json', command: 'lint:layers', root, error }); const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0); log( `layers: ${sum(counts)} import(s) against the layering in ${Object.keys(counts).length} file(s), baseline allows ${sum(baseline)}` ); - return grown.length ? 1 : 0; + return grown.length || stale.length ? 1 : 0; } diff --git a/client/scripts/lib/ratchet.mjs b/client/scripts/lib/ratchet.mjs index e6bd2085e..4cd11aeab 100644 --- a/client/scripts/lib/ratchet.mjs +++ b/client/scripts/lib/ratchet.mjs @@ -126,6 +126,50 @@ export function lowerCounts(baseline, counts, floor = 0) { return lowered; } +/** + * The entries of a count baseline that allow more than is measured now: a + * count that fell below its entry, one back at floor or under it, or a file + * that is gone. Exactly the entries lowerCounts would lower or drop. Each one + * fails its check until --update lowers it, or the count could grow back to + * the old entry unseen, and a deleted file would hand its allowance to the + * next file at its path. + * + * @param {Record} baseline + * @param {Record} counts + * @param {number | ((key: string) => number)} [floor] + * @returns {{ key: string, entry: number, now: number }[]} + */ +export function staleCounts(baseline, counts, floor = 0) { + const lowered = lowerCounts(baseline, counts, floor); + return Object.entries(baseline) + .filter(([key, entry]) => lowered[key] !== entry) + .map(([key, entry]) => ({ key, entry, now: counts[key] ?? 0 })); +} + +/** + * Prints a FAIL line for each stale entry (see staleCounts) of the baseline + * scripts/, then how to lower them. With root, an entry whose file is + * not there is called gone; label names an entry that is not a file path. + * + * @param {{ key: string, entry: number, now: number }[]} stale + * @param {{ file: string, command: string, root?: string, label?: (key: string) => string, error: (line: string) => void }} options + */ +export function reportStale(stale, { file, command, root, label = (key) => key, error }) { + for (const { key, entry, now } of stale) { + const gone = root !== undefined && !existsSync(join(root, key)); + error( + `FAIL ${label(key)} is held at ${entry} in scripts/${file}, ` + + (gone ? 'but the file is gone.' : `but there are ${now} now.`) + ); + } + if (stale.length) { + error( + `Run npm run ${command} -- --update to lower the baseline with the change that made it smaller: ` + + 'an entry above the count lets it grow back unseen.' + ); + } +} + /** The baseline entries that are still offenders: a list only ever loses files. */ export function lowerList(baseline, offenders) { const now = new Set(offenders); diff --git a/client/scripts/lib/rtl.mjs b/client/scripts/lib/rtl.mjs index a7f0a7710..a19d66cf8 100644 --- a/client/scripts/lib/rtl.mjs +++ b/client/scripts/lib/rtl.mjs @@ -9,6 +9,8 @@ import { lowerCounts, readBaseline, readText, + reportStale, + staleCounts, TEST_FILE, toKey, writeBaseline, @@ -163,9 +165,8 @@ export function check({ const grown = Object.entries(counts).filter(([file, n]) => n > (baseline[file] ?? 0)); const marked = Object.entries(markers).filter(([file, n]) => n > (allowedMarkers[file] ?? 0)); - const lowerable = - Object.entries(baseline).filter(([file, n]) => (counts[file] ?? 0) < n).length + - Object.entries(allowedMarkers).filter(([file, n]) => (markers[file] ?? 0) < n).length; + const stale = staleCounts(baseline, counts); + const staleMarkers = staleCounts(allowedMarkers, markers); for (const [file, n] of grown) { error( @@ -182,12 +183,11 @@ export function check({ '(a map, a measured position, a time axis) needs a reviewer to raise the entry by hand.' ); } - if (lowerable && !update) { - log(`${lowerable} entr(ies) are above what the files hold now: run with --update to lower them.`); - } + reportStale(stale, { file: 'rtl-baseline.json', command: 'lint:rtl', root: join(root, 'src'), error }); + reportStale(staleMarkers, { file: 'rtl-disable-baseline.json', command: 'lint:rtl', root: join(root, 'src'), error }); log( `rtl: ${sum(counts)} physical side(s) in ${Object.keys(counts).length} file(s), baseline allows ${sum(baseline)}; ` + `${sum(markers)} line(s) marked ${DISABLE}, baseline allows ${sum(allowedMarkers)}` ); - return grown.length || marked.length ? 1 : 0; + return grown.length || marked.length || stale.length || staleMarkers.length ? 1 : 0; } diff --git a/client/scripts/lib/size.mjs b/client/scripts/lib/size.mjs index af76b097a..ba2a94966 100644 --- a/client/scripts/lib/size.mjs +++ b/client/scripts/lib/size.mjs @@ -14,6 +14,8 @@ import { lowerCounts, readBaseline, readText, + reportStale, + staleCounts, TEST_FILE, toKey, writeBaseline, @@ -68,7 +70,9 @@ const limitOf = (key) => groupOf(key)?.limit ?? 0; /** * Runs the check against the baseline at baselinePath. With update, the - * baseline is lowered first. Returns the exit code. + * baseline is lowered first. An entry above the file's size now, or for a + * file that is gone, fails as well until --update lowers it. Returns the + * exit code. */ export function check({ root, @@ -86,7 +90,7 @@ export function check({ } const grown = Object.entries(sizes).filter(([key, n]) => n > Math.max(baseline[key] ?? 0, limitOf(key))); - const lowerable = Object.entries(baseline).filter(([key, n]) => (sizes[key] ?? 0) < n); + const stale = staleCounts(baseline, sizes, limitOf); for (const [key, n] of grown) { const entry = baseline[key]; @@ -96,11 +100,9 @@ export function check({ 'Move a concern into a module of its own instead of growing the file or packing its lines.' ); } - if (lowerable.length && !update) { - log(`${lowerable.length} file(s) are smaller than their baseline now: run with --update to lower it.`); - } + reportStale(stale, { file: 'size-baseline.json', command: 'lint:size', root, error }); log( `size: ${Object.keys(sizes).length} file(s), ${Object.keys(baseline).length} past their limit held at their baseline` ); - return grown.length ? 1 : 0; + return grown.length || stale.length ? 1 : 0; } diff --git a/client/scripts/lib/skips.mjs b/client/scripts/lib/skips.mjs index ade56a3c1..1975eb262 100644 --- a/client/scripts/lib/skips.mjs +++ b/client/scripts/lib/skips.mjs @@ -9,6 +9,8 @@ import { lowerCounts, readBaseline, readText, + reportStale, + staleCounts, TEST_FILE, toKey, writeBaseline, @@ -139,12 +141,11 @@ export function check({ if (grown.length) { error('Fix the test, delete it if its feature is gone, or use skipIf/runIf when it depends on the environment.'); } - const lowerable = Object.entries(baseline).filter(([key, n]) => (counts[key] ?? 0) < n); - if (lowerable.length && !update) - log(`${lowerable.length} file(s) skip fewer tests than the baseline: run with --update to lower it.`); + const stale = staleCounts(baseline, counts); + reportStale(stale, { file: 'skip-baseline.json', command: 'lint:skips', root, error }); const sum = (map) => Object.values(map).reduce((a, b) => a + b, 0); log( `skips: ${sum(counts)} skipped or todo test(s), baseline allows ${sum(baseline)}; ${Object.keys(only).length} file(s) with .only` ); - return grown.length || Object.keys(only).length ? 1 : 0; + return grown.length || stale.length || Object.keys(only).length ? 1 : 0; } diff --git a/client/scripts/rtl-lint.mjs b/client/scripts/rtl-lint.mjs index 9613e20d8..00d440423 100644 --- a/client/scripts/rtl-lint.mjs +++ b/client/scripts/rtl-lint.mjs @@ -17,7 +17,9 @@ * carries `rtl-lint-disable` in a comment (in a string it is no marker). The * marked lines are counted too, against scripts/rtl-disable-baseline.json, so * the marker cannot quietly take the place of the logical forms: a new one - * raises its file's entry by hand, where review sees it. + * raises its file's entry by hand, where review sees it. An entry of either + * baseline above what its file holds now, or for a file that is gone, fails + * as well until --update lowers it. * * npm run lint:rtl check against the baselines (CI) * npm run lint:rtl -- --list print every counted use, file by file diff --git a/client/scripts/size-lint.mjs b/client/scripts/size-lint.mjs index 48ddf624e..37a5e9d96 100644 --- a/client/scripts/size-lint.mjs +++ b/client/scripts/size-lint.mjs @@ -10,7 +10,10 @@ * that were already larger are listed in scripts/size-baseline.json with the * size they had, and the check fails when one of them grows past its entry. A * file that needs to grow is split by concern instead (the trip planner hook, - * page and road trip sidebar are the precedent). + * page and road trip sidebar are the precedent). An entry above the file's + * size now, or for a file that is gone, fails as well until --update lowers + * it: otherwise the file could grow back unseen, and a new file at a deleted + * path would inherit its allowance. * * npm run lint:size check against the baseline (CI) * npm run lint:size -- --update lower the baseline to what the files hold now; diff --git a/client/scripts/skip-lint.mjs b/client/scripts/skip-lint.mjs index ad53fe1ae..02580e688 100644 --- a/client/scripts/skip-lint.mjs +++ b/client/scripts/skip-lint.mjs @@ -7,6 +7,8 @@ * switched off and then forgotten (JourneyDetailPage.test.tsx has carried 35 * skipped blocks since April); the files that hold some are listed in * scripts/skip-baseline.json with their count, and a file may hold no more. + * An entry above what its file holds now, or for a file that is gone, fails + * as well until --update lowers it. * Playwright's test.fixme counts as a skip. Only a skip that decides at run * time is left out: skipIf and runIf, and in a Playwright spec * test.skip(condition, 'why'), unless the condition is a literal that always diff --git a/client/tests/unit/scripts/eslint-ratchet.test.ts b/client/tests/unit/scripts/eslint-ratchet.test.ts index c1641409c..aa9006a56 100644 --- a/client/tests/unit/scripts/eslint-ratchet.test.ts +++ b/client/tests/unit/scripts/eslint-ratchet.test.ts @@ -67,12 +67,23 @@ describe('lint:warnings', () => { expect(errors).toEqual([]); }); - it('ESLINT-002: passes when no count passes its entry', async () => { - const code = await run({ ...empty, src: { 'no-empty': 2 } }, (root) => [ + it('ESLINT-002: passes when every count sits at its entry', async () => { + const code = await run({ ...empty, src: { 'no-empty': 1 } }, (root) => [ result(root, 'src/a.ts', [warn('no-empty')]), ]); expect(code).toBe(0); - expect(tree.log.join('\n')).toMatch(/fell below the baseline/); + }); + + it('ESLINT-010: fails a count that fell below its entry, or a rule with none left, until --update', async () => { + const baseline = { ...empty, src: { 'no-empty': 2 }, suppressed: { 'react-hooks/exhaustive-deps': 1 } }; + const results = (root: string) => [result(root, 'src/a.ts', [warn('no-empty')])]; + expect(await run(baseline, results)).toBe(1); + const out = tree.error.join('\n'); + expect(out).toMatch(/src: no-empty is held at 2 in scripts\/eslint-baseline\.json, but there are 1 now/); + expect(out).toMatch(/suppressed: react-hooks\/exhaustive-deps is held at 1 .*but there are 0 now/); + expect(out).toMatch(/npm run lint:warnings -- --update/); + tree.remove(); + expect(await run(baseline, results, true)).toBe(0); }); it('ESLINT-003: fails a new warning and names the files behind it', async () => { diff --git a/client/tests/unit/scripts/layer-lint.test.ts b/client/tests/unit/scripts/layer-lint.test.ts index bbf0e63cd..ad39d81ea 100644 --- a/client/tests/unit/scripts/layer-lint.test.ts +++ b/client/tests/unit/scripts/layer-lint.test.ts @@ -120,6 +120,17 @@ describe('lint:layers', () => { expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual({ 'src/components/A.tsx': 1 }); }); + it('LAYERS-008: fails an entry above the imports a file holds, or for a file that is gone, until --update', () => { + const files = { 'src/components/A.tsx': "import { m } from '../pages/x/model'\n" }; + expect(run({ 'src/components/A.tsx': 2, 'src/components/Gone.tsx': 1 }, files)).toBe(1); + const out = tree.error.join('\n'); + expect(out).toMatch(/src\/components\/A\.tsx is held at 2 in scripts\/layers-baseline\.json, but there are 1 now/); + expect(out).toMatch(/src\/components\/Gone\.tsx is held at 1 .*but the file is gone/); + expect(out).toMatch(/npm run lint:layers -- --update/); + tree.remove(); + expect(run({ 'src/components/A.tsx': 2, 'src/components/Gone.tsx': 1 }, files, true)).toBe(0); + }); + it('LAYERS-007: help/ and vacay/ are helpers and import no view', () => { for (const helper of ['help', 'vacay']) { expect(RULES[helper as keyof typeof RULES]).toEqual( diff --git a/client/tests/unit/scripts/ratchet.test.ts b/client/tests/unit/scripts/ratchet.test.ts index ccaecb4b6..0b6a18005 100644 --- a/client/tests/unit/scripts/ratchet.test.ts +++ b/client/tests/unit/scripts/ratchet.test.ts @@ -11,6 +11,8 @@ import { RatchetError, readBaseline, readText, + reportStale, + staleCounts, writeBaseline, } from '../../../scripts/lib/ratchet.mjs'; import { ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture'; @@ -79,6 +81,36 @@ describe('scripts/lib/ratchet', () => { expect(lowerCounts({ a: 9 }, { a: 4 }, (key: string) => (key === 'a' ? 4 : 0))).toEqual({}); }); + it('RATCHET-010: an entry above the count, at the floor or for a gone file is stale, one at the count is not', () => { + expect( + staleCounts({ held: 5, shrank: 5, gone: 5, floor: 5 }, { held: 5, shrank: 3, floor: 2, fresh: 9 }, 2) + ).toEqual([ + { key: 'shrank', entry: 5, now: 3 }, + { key: 'gone', entry: 5, now: 0 }, + { key: 'floor', entry: 5, now: 2 }, + ]); + expect(staleCounts({ a: 3 }, { a: 4 })).toEqual([]); + }); + + it('RATCHET-011: a stale entry is reported with its count, or as gone, and how to lower it', () => { + tree = ratchetTree({ 'src/here.ts': 'x\n' }); + const lines: string[] = []; + const error = (line: string) => lines.push(line); + const stale = [ + { key: 'src/here.ts', entry: 4, now: 2 }, + { key: 'src/gone.ts', entry: 3, now: 0 }, + ]; + reportStale(stale, { file: 'b.json', command: 'lint:x', root: tree.root, error }); + expect(lines).toEqual([ + 'FAIL src/here.ts is held at 4 in scripts/b.json, but there are 2 now.', + 'FAIL src/gone.ts is held at 3 in scripts/b.json, but the file is gone.', + expect.stringMatching(/^Run npm run lint:x -- --update to lower the baseline/), + ]); + lines.length = 0; + reportStale([], { file: 'b.json', command: 'lint:x', error }); + expect(lines).toEqual([]); + }); + it('RATCHET-008: a file list only loses the files that stopped offending', () => { expect(lowerList(['a', 'b', 'c'], ['c', 'a', 'new'])).toEqual(['a', 'c']); }); diff --git a/client/tests/unit/scripts/rtl-lint.test.ts b/client/tests/unit/scripts/rtl-lint.test.ts index 12452aa2b..0211e65e7 100644 --- a/client/tests/unit/scripts/rtl-lint.test.ts +++ b/client/tests/unit/scripts/rtl-lint.test.ts @@ -76,6 +76,24 @@ describe('lint:rtl', () => { expect(JSON.parse(readFileSync(tree.path(MARKERS), 'utf8'))).toEqual({ 'a.tsx': 1 }); }); + it('RTL-009: fails an entry of either baseline above the file, or for a file that is gone, until --update', () => { + const files = { 'src/a.tsx': 'const a =
\nconst s = { left: 0 } // rtl-lint-disable\n' }; + expect(run({ 'a.tsx': 2 }, { 'a.tsx': 1 }, files)).toBe(1); + expect(tree.error.join('\n')).toMatch(/a\.tsx is held at 2 in scripts\/rtl-baseline\.json, but there are 1 now/); + tree.remove(); + expect(run({ 'a.tsx': 1 }, { 'a.tsx': 2 }, files)).toBe(1); + expect(tree.error.join('\n')).toMatch( + /a\.tsx is held at 2 in scripts\/rtl-disable-baseline\.json, but there are 1 now/ + ); + tree.remove(); + expect(run({ 'a.tsx': 1, 'gone.tsx': 3 }, { 'a.tsx': 1, 'gone.tsx': 1 }, files)).toBe(1); + const out = tree.error.join('\n'); + expect(out).toMatch(/gone\.tsx is held at 3 in scripts\/rtl-baseline\.json, but the file is gone/); + expect(out).toMatch(/gone\.tsx is held at 1 in scripts\/rtl-disable-baseline\.json, but the file is gone/); + tree.remove(); + expect(run({ 'a.tsx': 2, 'gone.tsx': 3 }, { 'a.tsx': 2, 'gone.tsx': 1 }, files, true)).toBe(0); + }); + it('RTL-007: a missing or broken baseline stops the check instead of reading as empty', () => { tree = ratchetTree({ [USES]: '{}', 'src/a.tsx': 'const a = 1\n' }); expect(() => diff --git a/client/tests/unit/scripts/size-lint.test.ts b/client/tests/unit/scripts/size-lint.test.ts index b0065374e..b29a0fc19 100644 --- a/client/tests/unit/scripts/size-lint.test.ts +++ b/client/tests/unit/scripts/size-lint.test.ts @@ -94,6 +94,23 @@ describe('lint:size', () => { expect(tree.error.join('\n')).toMatch(/src\/grew\.ts: 1150 lines/); }); + it('SIZE-011: fails an entry above the file it holds, or for a file that is gone, until --update', () => { + expect(run({ 'src/big.ts': 1200 }, { 'src/big.ts': lines(1100) })).toBe(1); + expect(tree.error.join('\n')).toMatch( + /src\/big\.ts is held at 1200 in scripts\/size-baseline\.json, but there are 1100 now/ + ); + expect(tree.error.join('\n')).toMatch(/npm run lint:size -- --update/); + tree.remove(); + // A deleted file would hand its allowance to the next file at its path. + expect(run({ 'src/gone.ts': 1200 }, { 'src/a.ts': lines(1) })).toBe(1); + expect(tree.error.join('\n')).toMatch(/src\/gone\.ts is held at 1200 .*but the file is gone/); + tree.remove(); + // Back under the limit is stale too: the entry would let it grow past the limit again. + expect(run({ 'src/big.ts': 1200 }, { 'src/big.ts': lines(900) })).toBe(1); + tree.remove(); + expect(run({ 'src/big.ts': 1200, 'src/gone.ts': 1200 }, { 'src/big.ts': lines(1100) }, true)).toBe(0); + }); + it('SIZE-009: a broken baseline stops the check', () => { tree = ratchetTree({ [BASELINE]: '{', 'src/a.ts': lines(1), 'tests/a.ts': lines(1), 'e2e/a.ts': lines(1) }); expect(() => check({ root: tree.root, baselinePath: tree.path(BASELINE), ...tree.out })).toThrow(RatchetError); diff --git a/client/tests/unit/scripts/skip-lint.test.ts b/client/tests/unit/scripts/skip-lint.test.ts index 567d2e644..3cc7cd2c1 100644 --- a/client/tests/unit/scripts/skip-lint.test.ts +++ b/client/tests/unit/scripts/skip-lint.test.ts @@ -93,6 +93,17 @@ describe('lint:skips', () => { expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual({ 'src/a.test.ts': 2 }); }); + it('SKIPS-010: fails an entry above the skips a file holds, or for a file that is gone, until --update', () => { + const files = { 'src/a.test.ts': "it.skip('a', () => {})\n" }; + expect(run({ 'src/a.test.ts': 2, 'tests/gone.test.ts': 1 }, files)).toBe(1); + const out = tree.error.join('\n'); + expect(out).toMatch(/src\/a\.test\.ts is held at 2 in scripts\/skip-baseline\.json, but there are 1 now/); + expect(out).toMatch(/tests\/gone\.test\.ts is held at 1 .*but the file is gone/); + expect(out).toMatch(/npm run lint:skips -- --update/); + tree.remove(); + expect(run({ 'src/a.test.ts': 2, 'tests/gone.test.ts': 1 }, files, true)).toBe(0); + }); + it('SKIPS-009: counts every vitest skip, whatever its title is', () => { const source = [ "const T = 'a'",