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'",