Files
Maurice 83d807781e ci(client): hold strict null check errors per file to a baseline that only shrinks
The client compiles with strict: false, so nullable contract fields arrive as
if always present and implicit anys stay invisible. tsconfig.strict.json is the
same program with strictNullChecks and noImplicitAny, and lint:strict
(scripts/strict-lint.mjs) counts its errors per file against
scripts/strict-baseline.json: a file may not gain one, a new file has to be
clean, and an entry above its count fails until --update lowers it. Nothing is
converted here; today's 534 errors in 165 files are frozen. allowJs stays: the
tests import the .mjs scripts and read their JSDoc types through it.
The todo MSW handler maps a cleared priority to its default so the test
helper written in this branch starts at zero. The check runs after the
typecheck in client-quality.
2026-10-10 21:32:59 +02:00

78 lines
3.5 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { afterEach, describe, expect, it } from 'vitest';
import { RatchetError } from '../../../scripts/lib/ratchet.mjs';
import { check, parseTscOutput } from '../../../scripts/lib/strict.mjs';
import { ratchetTree, type RatchetTree } from '../../helpers/ratchetFixture';
let tree: RatchetTree;
afterEach(() => tree?.remove());
const BASELINE = 'scripts/strict-baseline.json';
const OUTPUT = [
"src/a.ts(3,7): error TS18047: 'user' is possibly 'null'.",
"src/a.ts(9,1): error TS7006: Parameter 'x' implicitly has an 'any' type.",
"src\\b.tsx(1,2): error TS2322: Type 'string | null' is not assignable to type 'string'.",
" Type 'null' is not assignable to type 'string'.",
].join('\r\n');
function run(baseline: Record<string, number>, output: string, files: Record<string, string> = {}, update = false) {
tree = ratchetTree({ [BASELINE]: JSON.stringify(baseline), ...files });
return check({ root: tree.root, baselinePath: tree.path(BASELINE), update, tsc: () => output, ...tree.out });
}
describe('lint:strict', () => {
it('STRICT-001: counts errors per file, joins continuation lines and reads Windows paths', () => {
const { counts, listed, general } = parseTscOutput(OUTPUT);
expect(counts).toEqual({ 'src/a.ts': 2, 'src/b.tsx': 1 });
expect(listed['src/a.ts'][0]).toBe("3:7 TS18047 'user' is possibly 'null'.");
expect(general).toEqual([]);
});
it('STRICT-002: passes counts within the baseline', () => {
const files = { 'src/a.ts': '', 'src/b.tsx': '' };
expect(run({ 'src/a.ts': 2, 'src/b.tsx': 1 }, OUTPUT, files)).toBe(0);
expect(tree.log.join('\n')).toMatch(/strict: 3 error\(s\) in 2 file\(s\).*baseline allows 3/);
});
it('STRICT-003: a new file starts at zero, and a file that gains an error fails', () => {
expect(run({ 'src/a.ts': 2 }, OUTPUT, { 'src/a.ts': '', 'src/b.tsx': '' })).toBe(1);
expect(tree.error.join('\n')).toMatch(
/src\/b\.tsx: 1 error\(s\) under strictNullChecks and noImplicitAny, baseline 0/
);
});
it('STRICT-004: an error tied to no file fails the check', () => {
expect(
run({}, "tsconfig.strict.json(3,5): error TS5023: Unknown compiler option 'x'.\nerror TS6053: File not found.")
).toBe(1);
expect(tree.error.join('\n')).toMatch(/FAIL {2}error TS6053: File not found\./);
});
it('STRICT-005: --update lowers the baseline and never raises it', () => {
run({ 'src/a.ts': 5, 'src/b.tsx': 1, 'src/fixed.ts': 2 }, OUTPUT, { 'src/a.ts': '', 'src/b.tsx': '' }, true);
expect(JSON.parse(readFileSync(tree.path(BASELINE), 'utf8'))).toEqual({ 'src/a.ts': 2, 'src/b.tsx': 1 });
});
it('STRICT-006: fails an entry above its count, or for a file that is gone, until --update', () => {
expect(run({ 'src/a.ts': 3, 'src/b.tsx': 1 }, OUTPUT, { 'src/a.ts': '', 'src/b.tsx': '' })).toBe(1);
expect(tree.error.join('\n')).toMatch(
/src\/a\.ts is held at 3 in scripts\/strict-baseline\.json, but there are 2 now/
);
tree.remove();
expect(run({ 'src/a.ts': 2, 'src/b.tsx': 1, 'src/gone.ts': 1 }, OUTPUT, { 'src/a.ts': '', 'src/b.tsx': '' })).toBe(
1
);
expect(tree.error.join('\n')).toMatch(
/src\/gone\.ts is held at 1 in scripts\/strict-baseline\.json, but the file is gone/
);
});
it('STRICT-007: a missing baseline stops the check instead of reading as empty', () => {
tree = ratchetTree({});
expect(() => check({ root: tree.root, baselinePath: tree.path(BASELINE), tsc: () => '', ...tree.out })).toThrow(
RatchetError
);
});
});