From 78b9ab36e75ed80eefb956306121f917cbd78faa Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 29 Jul 2026 11:57:45 +1000 Subject: [PATCH 1/3] test(scripts): cover repository-linter race guards --- .../lint-no-hardcoded-runners.test.mjs | 37 ++++++++++++++++++- scripts/__tests__/vitest-config.test.mjs | 8 ++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 scripts/__tests__/vitest-config.test.mjs diff --git a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs index 709ac63ed..a2c1a5d8e 100644 --- a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs +++ b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs @@ -12,8 +12,10 @@ const SCRIPT = resolve( function runScript(script, ...targets) { try { - execFileSync(process.execPath, [script, ...targets], { encoding: 'utf8' }) - return { exitCode: 0, output: '' } + const output = execFileSync(process.execPath, [script, ...targets], { + encoding: 'utf8', + }) + return { exitCode: 0, output } } catch (err) { return { exitCode: err.status, @@ -26,6 +28,25 @@ function run(target) { return runScript(SCRIPT, target) } +function runWithReaddirError(code) { + const probe = resolve( + fileURLToPath(import.meta.url), + '../../walk-error-probe.mjs', + ) + const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-walk-')) + const src = readFileSync(SCRIPT, 'utf8').replace( + "import { readdir } from 'node:fs/promises'", + `async function readdir() { const err = new Error('readdir failed'); err.code = '${code}'; throw err }`, + ) + try { + writeFileSync(probe, src) + return runScript(probe, dir) + } finally { + rmSync(probe, { force: true }) + rmSync(dir, { recursive: true, force: true }) + } +} + describe('lint-no-hardcoded-runners', () => { const fx = (name) => resolve(fileURLToPath(import.meta.url), `../fixtures/${name}`) @@ -51,6 +72,18 @@ describe('lint-no-hardcoded-runners', () => { expect(r.output).not.toMatch(/at ModuleJob/) }) + it('skips a directory that vanished while being walked', () => { + const r = runWithReaddirError('ENOENT') + expect(r.exitCode).toBe(0) + expect(r.output).toContain('OK') + }) + + it('rethrows unexpected errors encountered while walking', () => { + const r = runWithReaddirError('EACCES') + expect(r.exitCode).not.toBe(0) + expect(r.output).toContain('EACCES') + }) + // Matches the sibling linter: a target outside the repo rendered as a // `../../../../..` chain out of the root instead of naming the file. it('renders an absolute path for an offender outside the repo root', () => { diff --git a/scripts/__tests__/vitest-config.test.mjs b/scripts/__tests__/vitest-config.test.mjs new file mode 100644 index 000000000..1ad8a56da --- /dev/null +++ b/scripts/__tests__/vitest-config.test.mjs @@ -0,0 +1,8 @@ +import { describe, expect, it } from 'vitest' +import scriptsVitestConfig from '../vitest.config.mjs' + +describe('scripts vitest config', () => { + it('keeps script test files serialized', () => { + expect(scriptsVitestConfig.test.fileParallelism).toBe(false) + }) +}) From 2d21debf4fd9da0d76436e967a25a97f27edcac7 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 29 Jul 2026 12:05:39 +1000 Subject: [PATCH 2/3] test(scripts): cover nested filesystem races --- .../lint-no-hardcoded-runners.test.mjs | 64 +++++++++++++++++-- scripts/lint-no-hardcoded-runners.mjs | 12 +++- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs index a2c1a5d8e..9c93b8e1d 100644 --- a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs +++ b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs @@ -28,16 +28,60 @@ function run(target) { return runScript(SCRIPT, target) } -function runWithReaddirError(code) { +function runWithReaddirError(code, { afterParent = false } = {}) { const probe = resolve( fileURLToPath(import.meta.url), '../../walk-error-probe.mjs', ) const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-walk-')) + const replacement = afterParent + ? `let readdirCalls = 0 +async function readdir() { + readdirCalls += 1 + if (readdirCalls === 1) { + return [{ name: 'vanished', isDirectory: () => true }] + } + const err = new Error('readdir failed') + err.code = '${code}' + throw err +}` + : `async function readdir() { const err = new Error('readdir failed'); err.code = '${code}'; throw err }` const src = readFileSync(SCRIPT, 'utf8').replace( "import { readdir } from 'node:fs/promises'", - `async function readdir() { const err = new Error('readdir failed'); err.code = '${code}'; throw err }`, + replacement, + ) + try { + writeFileSync(probe, src) + return runScript(probe, dir) + } finally { + rmSync(probe, { force: true }) + rmSync(dir, { recursive: true, force: true }) + } +} + +function runWithReadFileError(code) { + const probe = resolve( + fileURLToPath(import.meta.url), + '../../read-error-probe.mjs', ) + const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-read-')) + const src = readFileSync(SCRIPT, 'utf8') + .replace( + "import { readFileSync, statSync } from 'node:fs'", + `import { readFileSync as realReadFileSync, statSync } from 'node:fs' +function readFileSync(path, ...args) { + if (String(path).endsWith('vanished.ts')) { + const err = new Error('read failed') + err.code = '${code}' + throw err + } + return realReadFileSync(path, ...args) +}`, + ) + .replace( + "import { readdir } from 'node:fs/promises'", + "async function readdir() { return [{ name: 'vanished.ts', isDirectory: () => false }] }", + ) try { writeFileSync(probe, src) return runScript(probe, dir) @@ -72,8 +116,8 @@ describe('lint-no-hardcoded-runners', () => { expect(r.output).not.toMatch(/at ModuleJob/) }) - it('skips a directory that vanished while being walked', () => { - const r = runWithReaddirError('ENOENT') + it('skips a directory that vanished after its parent was enumerated', () => { + const r = runWithReaddirError('ENOENT', { afterParent: true }) expect(r.exitCode).toBe(0) expect(r.output).toContain('OK') }) @@ -84,6 +128,18 @@ describe('lint-no-hardcoded-runners', () => { expect(r.output).toContain('EACCES') }) + it('skips a file that vanished after its directory was enumerated', () => { + const r = runWithReadFileError('ENOENT') + expect(r.exitCode).toBe(0) + expect(r.output).toContain('OK') + }) + + it('rethrows unexpected errors reading an enumerated file', () => { + const r = runWithReadFileError('EACCES') + expect(r.exitCode).not.toBe(0) + expect(r.output).toContain('EACCES') + }) + // Matches the sibling linter: a target outside the repo rendered as a // `../../../../..` chain out of the root instead of naming the file. it('renders an absolute path for an offender outside the repo root', () => { diff --git a/scripts/lint-no-hardcoded-runners.mjs b/scripts/lint-no-hardcoded-runners.mjs index 26c6a2548..bdf058650 100644 --- a/scripts/lint-no-hardcoded-runners.mjs +++ b/scripts/lint-no-hardcoded-runners.mjs @@ -159,7 +159,17 @@ for (const target of TARGETS) { const shown = rel.startsWith('..') ? file : rel if (ALLOWLISTED_PATHS.has(rel)) continue if (/\.(test|spec)\.(ts|tsx|mts|cts)$/.test(file)) continue - const lines = readFileSync(file, 'utf8').split('\n') + let source + try { + source = readFileSync(file, 'utf8') + } catch (err) { + // A file can vanish after its directory was enumerated, just as a + // directory can vanish before the recursive readdir above. There is + // nothing left to lint; unexpected read failures must still surface. + if (err?.code === 'ENOENT') continue + throw err + } + const lines = source.split('\n') lines.forEach((line, idx) => { const matches = NPX_TOKEN.test(line) if (!matches) return From 9c7ff43b329ffd72a9459b72f1663a3c3c58c669 Mon Sep 17 00:00:00 2001 From: Toby Hede Date: Wed, 29 Jul 2026 12:08:48 +1000 Subject: [PATCH 3/3] test(scripts): capture successful stderr output --- .../lint-no-hardcoded-runners.test.mjs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs index 9c93b8e1d..d13c2ad9f 100644 --- a/scripts/__tests__/lint-no-hardcoded-runners.test.mjs +++ b/scripts/__tests__/lint-no-hardcoded-runners.test.mjs @@ -1,4 +1,4 @@ -import { execFileSync } from 'node:child_process' +import { spawnSync } from 'node:child_process' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' @@ -11,16 +11,12 @@ const SCRIPT = resolve( ) function runScript(script, ...targets) { - try { - const output = execFileSync(process.execPath, [script, ...targets], { - encoding: 'utf8', - }) - return { exitCode: 0, output } - } catch (err) { - return { - exitCode: err.status, - output: String(err.stdout) + String(err.stderr), - } + const result = spawnSync(process.execPath, [script, ...targets], { + encoding: 'utf8', + }) + return { + exitCode: result.status, + output: String(result.stdout) + String(result.stderr), } } @@ -99,6 +95,19 @@ describe('lint-no-hardcoded-runners', () => { expect(run(fx('clean.ts')).exitCode).toBe(0) }) + it('captures stderr from a successful script', () => { + const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-stderr-')) + const probe = join(dir, 'stderr-probe.mjs') + try { + writeFileSync(probe, "process.stderr.write('successful warning\\n')\n") + const r = runScript(probe) + expect(r.exitCode).toBe(0) + expect(r.output).toContain('successful warning') + } finally { + rmSync(dir, { recursive: true, force: true }) + } + }) + it('fails on a hardcoded `npx ...` string literal', () => { const r = run(fx('offender.ts')) expect(r.exitCode).toBe(1)