test(plugin-patterns): scope win32 shell-flag regex per call site

Codex review on #2722 correctly flagged that [\s\S]*? could cross call
boundaries — dropping the shell flag from runTypeCheck could still pass
if runTests or runLint kept theirs, because the lazy match would just
continue past the end of the target call until it found the flag in a
sibling options object.

Replaced [\s\S]*? with [^}]*? and anchored the match to this call's own
options-object boundaries (\{ … \}\s*\);), so each assertion fails
independently when its target call loses the flag. Keep the option
objects flat — nested braces would terminate [^}]*? prematurely.

Confidence: high
Scope-risk: narrow
Directive: If future option objects nest braces (e.g. { env: { … } }),
  switch these to balanced-scan or per-call substring extraction —
  [^}]*? stops at the first inner }.
Not-tested: Mutation-test that synthetically removes each shell flag
  and asserts each test fails independently.
This commit is contained in:
Victor de Andrade
2026-04-18 12:19:15 +02:00
parent bfec081caf
commit a5def5f4e9

View File

@@ -269,27 +269,32 @@ describe('win32 spawn hardening (#2721)', () => {
// below spawn npm / npx, which resolve to npm.cmd / npx.cmd on Windows, so
// each one needs shell:true gated on win32. CI is Ubuntu-only, so static
// source assertions are the only regression guard.
//
// Each regex is scoped to a single options object via [^}]*? — if the shell
// flag is dropped from this specific call site, the match cannot silently
// succeed by finding the same flag in a sibling call below. Keep the
// option objects flat (no nested braces) so this scoping holds.
const testDirPath = dirname(fileURLToPath(import.meta.url));
const sourcePath = join(testDirPath, '..', '..', 'hooks', 'plugin-patterns', 'index.ts');
it('runTypeCheck spawnSync("npx", …) must pass shell:true on win32', () => {
const src = readFileSync(sourcePath, 'utf-8');
expect(src).toMatch(
/spawnSync\('npx', \['tsc', '--noEmit'\][\s\S]*?shell:\s*process\.platform === 'win32'/
/spawnSync\('npx', \['tsc', '--noEmit'\], \{[^}]*?shell:\s*process\.platform === 'win32'[^}]*?\}\s*\);/
);
});
it('runTests execFileSync("npm test", …) must pass shell:true on win32', () => {
const src = readFileSync(sourcePath, 'utf-8');
expect(src).toMatch(
/execFileSync\('npm', \['test'\][\s\S]*?shell:\s*process\.platform === 'win32'/
/execFileSync\('npm', \['test'\], \{[^}]*?shell:\s*process\.platform === 'win32'[^}]*?\}\s*\);/
);
});
it('runLint execFileSync("npm run lint", …) must pass shell:true on win32', () => {
const src = readFileSync(sourcePath, 'utf-8');
expect(src).toMatch(
/execFileSync\('npm', \['run', 'lint'\][\s\S]*?shell:\s*process\.platform === 'win32'/
/execFileSync\('npm', \['run', 'lint'\], \{[^}]*?shell:\s*process\.platform === 'win32'[^}]*?\}\s*\);/
);
});
});