import { describe, expect, test } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { compareDiagnostics, parseDiagnostics, readBaseline } from '../scripts/typecheck-test'; const output = [ "test/a.test.ts(3,5): error TS2339: Property 'questions' does not exist on type 'Plan'.", "test/a.test.ts(9,5): error TS2339: Property 'questions' does not exist on type 'Plan'.", 'test/b.test.ts(1,1): error TS2345: Argument of type \'string\' is not assignable to parameter of type \'number\'.', " Type 'string' is not assignable to type 'number'.", 'Found 3 errors.', ].join('\n'); describe('test typecheck ratchet', () => { test('counts repeated identical diagnostics instead of collapsing them', () => { expect(parseDiagnostics(output)).toEqual({ "test/a.test.ts\tTS2339\tProperty 'questions' does not exist on type 'Plan'.": 2, "test/b.test.ts\tTS2345\tArgument of type 'string' is not assignable to parameter of type 'number'. Type 'string' is not assignable to type 'number'.": 1, }); }); test('strips the checkout root from messages so every clone shares one identity', () => { const line = "test/d.test.ts(1,1): error TS2322: Type 'import(\"/tmp/clone-a/lib/x\").A' is not assignable to type 'B'."; const a = parseDiagnostics(line, '/tmp/clone-a'); const b = parseDiagnostics(line.replace('/tmp/clone-a', '/home/ci/work/gstack'), '/home/ci/work/gstack'); expect(Object.keys(a)).toEqual(Object.keys(b)); expect(Object.keys(a)[0]).toContain('import(\"lib/x\")'); }); test('ignores line and column so moving code does not churn the baseline', () => { const moved = output.replace('(3,5)', '(30,7)').replace('(9,5)', '(90,1)'); expect(parseDiagnostics(moved)).toEqual(parseDiagnostics(output)); }); test('a duplicate of an existing diagnostic and a new identity both fail', () => { const baseline = parseDiagnostics(output); const current = parseDiagnostics(output + "\ntest/a.test.ts(12,5): error TS2339: Property 'questions' does not exist on type 'Plan'.\ntest/c.test.ts(1,1): error TS2304: Cannot find name 'join'."); const { added, fixed } = compareDiagnostics(baseline, current); expect(added.map(d => [d.identity.split('\t')[0], d.baseline, d.current])).toEqual([ ['test/a.test.ts', 2, 3], ['test/c.test.ts', 0, 1], ]); expect(fixed).toEqual([]); }); test('a fixed diagnostic is reported so the smaller allowance gets locked in', () => { const baseline = parseDiagnostics(output); const { added, fixed } = compareDiagnostics(baseline, parseDiagnostics(output.split('\n').slice(1).join('\n'))); expect(added).toEqual([]); expect(fixed).toHaveLength(1); expect(fixed[0]).toMatchObject({ baseline: 2, current: 1 }); }); test('a missing or malformed baseline fails closed with the regeneration command', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'typecheck-baseline-')); try { expect(() => readBaseline(path.join(dir, 'missing.json'))).toThrow('bun run typecheck:test --write-baseline'); fs.writeFileSync(path.join(dir, 'bad.json'), JSON.stringify({ version: 1, diagnostics: { x: 0 } })); expect(() => readBaseline(path.join(dir, 'bad.json'))).toThrow('malformed'); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); test('the committed baseline is readable and non-empty', () => { const baseline = readBaseline(path.join(import.meta.dir, '..', 'scripts', 'typecheck-test-baseline.json')); expect(Object.keys(baseline).length).toBeGreaterThan(0); }); });