From 1008e7497292fd5aa818d84cfe478fb4c77ecd97 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 5 Nov 2026 21:50:42 +0000 Subject: [PATCH 1/3] fix: make npm run clean remove the build info files --- package.json | 2 +- test/unit/tools/clean-script.test.ts | 68 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 test/unit/tools/clean-script.test.ts diff --git a/package.json b/package.json index 9a28beee..61942702 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ }, "scripts": { "build": "tsc -p tsconfig.build.json && node ./scripts/copy-renderer-assets.mjs", - "clean": "rimraf coverage dist *.tsbuildinfo", + "clean": "rimraf coverage dist .tsconfig.tsbuildinfo .tsconfig.build.tsbuildinfo", "cli": "tsx src/cli/main.ts", "doctor": "tsx src/cli/main.ts doctor", "format": "oxfmt . --write", diff --git a/test/unit/tools/clean-script.test.ts b/test/unit/tools/clean-script.test.ts new file mode 100644 index 00000000..2c0ede7e --- /dev/null +++ b/test/unit/tools/clean-script.test.ts @@ -0,0 +1,68 @@ +import { execFileSync } from 'node:child_process'; +import { existsSync, readFileSync } from 'node:fs'; +import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { delimiter, join, resolve } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +const repoRoot = resolve(import.meta.dirname, '../../..'); +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all( + tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })), + ); +}); + +function buildInfoFiles(): string[] { + return ['tsconfig.json', 'tsconfig.build.json'].map((name) => { + const text = readFileSync(join(repoRoot, name), 'utf8'); + const file = /"tsBuildInfoFile":\s*"([^"]+)"/.exec(text)?.[1]; + if (file === undefined) { + throw new Error(`${name} names no tsBuildInfoFile`); + } + return file; + }); +} + +describe('npm run clean', () => { + // A build info file that survives `clean` makes the next `tsc -p` build + // skip its output, so `clean` then `build` would leave dist/ empty. + it('removes dist, coverage and every build info file the tsconfigs name', async () => { + const pkg = JSON.parse( + readFileSync(join(repoRoot, 'package.json'), 'utf8'), + ) as { scripts: Record }; + const script = pkg.scripts.clean; + if (script === undefined) { + throw new Error('package.json has no clean script'); + } + + const dir = await realpath( + await mkdtemp(join(tmpdir(), 'agent-tty-clean-')), + ); + tempDirs.push(dir); + const targets = [ + 'dist/cli/main.js', + 'coverage/index.html', + ...buildInfoFiles(), + ]; + for (const target of targets) { + await mkdir(join(dir, target, '..'), { recursive: true }); + await writeFile(join(dir, target), 'stale'); + } + + execFileSync('sh', ['-c', script], { + cwd: dir, + env: { + ...process.env, + PATH: `${join(repoRoot, 'node_modules', '.bin')}${delimiter}${process.env.PATH ?? ''}`, + }, + stdio: 'pipe', + }); + + for (const target of ['dist', 'coverage', ...buildInfoFiles()]) { + expect(existsSync(join(dir, target)), `${target} removed`).toBe(false); + } + }); +}); From 8d06ff66cc25384256d05f9999323b15ce4f166a Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 5 Nov 2026 23:10:10 +0000 Subject: [PATCH 2/3] test: check that npm run clean keeps everything else --- test/unit/tools/clean-script.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unit/tools/clean-script.test.ts b/test/unit/tools/clean-script.test.ts index 2c0ede7e..bebdbcab 100644 --- a/test/unit/tools/clean-script.test.ts +++ b/test/unit/tools/clean-script.test.ts @@ -29,7 +29,7 @@ function buildInfoFiles(): string[] { describe('npm run clean', () => { // A build info file that survives `clean` makes the next `tsc -p` build // skip its output, so `clean` then `build` would leave dist/ empty. - it('removes dist, coverage and every build info file the tsconfigs name', async () => { + it('removes dist, coverage and every build info file the tsconfigs name, and nothing else', async () => { const pkg = JSON.parse( readFileSync(join(repoRoot, 'package.json'), 'utf8'), ) as { scripts: Record }; @@ -42,10 +42,12 @@ describe('npm run clean', () => { await mkdtemp(join(tmpdir(), 'agent-tty-clean-')), ); tempDirs.push(dir); + const kept = ['package.json', 'src/cli/main.ts', 'visible.tsbuildinfo']; const targets = [ 'dist/cli/main.js', 'coverage/index.html', ...buildInfoFiles(), + ...kept, ]; for (const target of targets) { await mkdir(join(dir, target, '..'), { recursive: true }); @@ -64,5 +66,8 @@ describe('npm run clean', () => { for (const target of ['dist', 'coverage', ...buildInfoFiles()]) { expect(existsSync(join(dir, target)), `${target} removed`).toBe(false); } + for (const target of kept) { + expect(existsSync(join(dir, target)), `${target} kept`).toBe(true); + } }); }); From 54dd487b2858178b5f03627afeb2a5729a333c80 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 5 Nov 2026 23:29:52 +0000 Subject: [PATCH 3/3] test: run the clean script through the platform shell --- test/unit/tools/clean-script.test.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/unit/tools/clean-script.test.ts b/test/unit/tools/clean-script.test.ts index bebdbcab..1be30063 100644 --- a/test/unit/tools/clean-script.test.ts +++ b/test/unit/tools/clean-script.test.ts @@ -1,4 +1,4 @@ -import { execFileSync } from 'node:child_process'; +import { execSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; @@ -54,14 +54,18 @@ describe('npm run clean', () => { await writeFile(join(dir, target), 'stale'); } - execFileSync('sh', ['-c', script], { - cwd: dir, - env: { - ...process.env, - PATH: `${join(repoRoot, 'node_modules', '.bin')}${delimiter}${process.env.PATH ?? ''}`, - }, - stdio: 'pipe', - }); + // npm runs scripts through /bin/sh, or cmd.exe on Windows; execSync + // picks the same shell. The variable can be spelled Path or PATH, and a + // shell can carry both, so prepend the bin folder to each spelling. + const bin = join(repoRoot, 'node_modules', '.bin'); + const env: NodeJS.ProcessEnv = { ...process.env }; + const pathKeys = Object.keys(env).filter( + (key) => key.toUpperCase() === 'PATH', + ); + for (const key of pathKeys.length > 0 ? pathKeys : ['PATH']) { + env[key] = `${bin}${delimiter}${env[key] ?? ''}`; + } + execSync(script, { cwd: dir, env, stdio: 'pipe' }); for (const target of ['dist', 'coverage', ...buildInfoFiles()]) { expect(existsSync(join(dir, target)), `${target} removed`).toBe(false);