From c4f33a044cafb705e0e30d2f98659421fbb1400d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 28 Aug 2026 03:43:27 -0700 Subject: [PATCH] Make baseline regeneration discoverable and cover the CI guard Drift failures are routine here: any PR that incidentally fixes an e2e or mobile type error trips the stale-entry check. Finding the fix meant opening the script to learn --write exists, so the failure now prints the exact regenerate command, and both packages expose it as a script. Resolve the env-supplied PR base against the repo root like the flag already was. Mobile CI runs with working-directory: mobile, so a repo-relative value would have resolved against the wrong directory. Cover the growth guard on the check path and the env form CI actually uses; both branches were previously exercised only through --write. --- .../scripts/typecheck-diagnostic-baseline.mjs | 13 +++- .../typecheck-diagnostic-baseline.test.mjs | 76 ++++++++++++++++++- mobile/package.json | 1 + package.json | 1 + 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/config/scripts/typecheck-diagnostic-baseline.mjs b/config/scripts/typecheck-diagnostic-baseline.mjs index e7198b816c7..727b686a55f 100644 --- a/config/scripts/typecheck-diagnostic-baseline.mjs +++ b/config/scripts/typecheck-diagnostic-baseline.mjs @@ -136,6 +136,12 @@ function subtractDiagnostics(left, right) { }) } +/** Failures here are routine, so the message carries its own fix. */ +function regenerationHint() { + const argv = process.argv.slice(2).filter((value) => value !== '--write') + return `Regenerate with: node ${relative(repoRoot, import.meta.filename)} ${argv.join(' ')} --write` +} + function formatDiagnostic(diagnostic) { const location = diagnostic.file ? `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}` @@ -168,9 +174,9 @@ async function main() { const options = parseArgs(process.argv.slice(2)) const projectPath = resolve(repoRoot, options.project) const baselinePath = resolve(repoRoot, options.baseline) - const baseBaselinePath = options.baseBaseline - ? resolve(repoRoot, options.baseBaseline) - : process.env.TYPECHECK_BASELINE_BASE_PATH + // Resolve the env form like the flag: mobile runs with working-directory: mobile. + const baseBaselineOption = options.baseBaseline ?? process.env.TYPECHECK_BASELINE_BASE_PATH + const baseBaselinePath = baseBaselineOption ? resolve(repoRoot, baseBaselineOption) : undefined const tscPath = resolve(repoRoot, options.tsc ?? 'node_modules/typescript/bin/tsc') const diagnostics = collectDiagnostics(projectPath, tscPath) const baseBaseline = baseBaselinePath ? await readBaseline(baseBaselinePath) : null @@ -207,6 +213,7 @@ async function main() { console.error(formatDiagnostic(diagnostic)) } } + console.error(regenerationHint()) process.exitCode = 1 } diff --git a/config/scripts/typecheck-diagnostic-baseline.test.mjs b/config/scripts/typecheck-diagnostic-baseline.test.mjs index 43a8ad84f6a..f261a6b05f5 100644 --- a/config/scripts/typecheck-diagnostic-baseline.test.mjs +++ b/config/scripts/typecheck-diagnostic-baseline.test.mjs @@ -47,7 +47,7 @@ function runBaseline(paths, extraArgs = [], spawnOptions = {}) { ...extraArgs ], { - cwd: repoRoot, + cwd: spawnOptions.cwd ?? repoRoot, encoding: 'utf8', env: { ...process.env, ...spawnOptions.env } } @@ -151,6 +151,80 @@ describe('typecheck diagnostic baseline', () => { expect(result.stdout).toContain('1 known diagnostics, no drift') }) + it('rejects a committed baseline that grew from the PR base', () => { + const paths = createFixture("const first: number = 'bad'\n") + expect(runBaseline(paths, ['--write']).status).toBe(0) + // Base knows only the first error; the committed baseline was raised to cover a second. + const base = JSON.parse(readFileSync(paths.baseline, 'utf8')) + writeFileSync(paths.baseBaseline, JSON.stringify(base)) + writeFileSync(paths.source, "const first: number = 'bad'\nconst second: boolean = 'also-bad'\n") + expect(runBaseline(paths, ['--write', '--base-baseline', paths.baseBaseline]).status).toBe(1) + writeFileSync( + paths.baseline, + JSON.stringify({ + version: 1, + diagnostics: [ + ...base.diagnostics, + { + file: 'source.ts', + line: 2, + column: 7, + code: 2322, + message: "Type 'string' is not assignable to type 'boolean'." + } + ] + }) + ) + + const check = runBaseline(paths, ['--base-baseline', paths.baseBaseline]) + + expect(check.status).toBe(1) + expect(check.stderr).toContain('Diagnostic baseline grew (1)') + }) + + it('reads the PR base from the environment the way CI passes it', () => { + const paths = createFixture("const value: number = 'bad'\n") + expect(runBaseline(paths, ['--write']).status).toBe(0) + writeFileSync(paths.baseBaseline, JSON.stringify({ version: 1, diagnostics: [] })) + + const result = runBaseline(paths, [], { + env: { TYPECHECK_BASELINE_BASE_PATH: paths.baseBaseline } + }) + + expect(result.status).toBe(1) + expect(result.stderr).toContain('Diagnostic baseline grew (1)') + }) + + it('resolves the env-supplied PR base against the repo, not the working directory', () => { + // Mobile CI runs this with working-directory: mobile, so a repo-relative base + // path must not resolve against the caller's cwd. + const result = runBaseline( + { + project: 'config/tsconfig.e2e.json', + baseline: 'config/typecheck-e2e-diagnostics.json' + }, + [], + { + cwd: join(repoRoot, 'mobile'), + env: { TYPECHECK_BASELINE_BASE_PATH: 'config/typecheck-e2e-diagnostics.json' } + } + ) + + expect(result.stderr).not.toContain('ENOENT') + expect(result.status).toBe(0) + }) + + it('names the regeneration command when the baseline drifts', () => { + const paths = createFixture("const value: number = 'bad'\n") + expect(runBaseline(paths, ['--write']).status).toBe(0) + writeFileSync(paths.source, 'const value: number = 1\n') + + const stale = runBaseline(paths) + + expect(stale.status).toBe(1) + expect(stale.stderr).toContain('--write') + }) + it('refuses to regenerate a baseline that grows from the PR base', () => { const paths = createFixture("const first: number = 'bad'\n") expect(runBaseline(paths, ['--write']).status).toBe(0) diff --git a/mobile/package.json b/mobile/package.json index f7fef14b363..d15596498e0 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -10,6 +10,7 @@ "postinstall": "node scripts/build-terminal-webview-engine.mjs && node scripts/build-mermaid-webview-engine.mjs", "test": "vitest run", "typecheck": "node ../config/scripts/typecheck-diagnostic-baseline.mjs --project mobile/tsconfig.json --baseline mobile/typecheck-test-diagnostics.json --tsc mobile/node_modules/typescript/bin/tsc", + "typecheck:write": "node ../config/scripts/typecheck-diagnostic-baseline.mjs --project mobile/tsconfig.json --baseline mobile/typecheck-test-diagnostics.json --tsc mobile/node_modules/typescript/bin/tsc --write", "lint": "oxlint", "format": "oxfmt --write .", "format:check": "oxfmt --check .", diff --git a/package.json b/package.json index 60bb6548398..a792d7a28af 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "typecheck:cli": "tsc --noEmit -p config/tsconfig.tc.cli.json", "typecheck:web": "tsc --noEmit -p config/tsconfig.tc.web.json", "typecheck:e2e": "node config/scripts/typecheck-diagnostic-baseline.mjs --project config/tsconfig.e2e.json --baseline config/typecheck-e2e-diagnostics.json", + "typecheck:e2e:write": "node config/scripts/typecheck-diagnostic-baseline.mjs --project config/tsconfig.e2e.json --baseline config/typecheck-e2e-diagnostics.json --write", "typecheck": "node config/scripts/run-typecheck-projects-in-parallel.mjs", "typecheck:tsc:node": "tsc --noEmit -p config/tsconfig.node.json --composite false", "typecheck:tsc:cli": "tsc --noEmit -p config/tsconfig.cli.json --composite false",