From 4f4654bf212e9b82e03f9aea24a7cd59e607e2ec Mon Sep 17 00:00:00 2001 From: Jamkris Date: Thu, 14 May 2026 11:22:44 +0900 Subject: [PATCH] test(hooks): regression coverage for dev-server-block subshell bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lock in the behavior added by the previous commit. Each new case was verified to fail before the fix and pass after. Bypasses now blocked (exit 2): - \$(npm run dev) command substitution - \`npm run dev\` backtick substitution - echo \$(npm run dev) substitution inside an argument - (npm run dev) plain subshell group - \$(echo a; npm run dev) substitution containing a sequenced segment - (pnpm dev) plain subshell group, alt package manager Allow cases — explicitly proven NOT to regress so the fix doesn't over-block legitimate uses: - (tmux new-session -d -s dev "npm run dev") tmux launcher inside () - git commit -m '(npm run dev)' literal in single quotes - echo "(npm run dev)" literal in double quotes (bash does NOT subshell () inside double quotes) - git commit -m '\$(npm run dev) fix' literal in single quotes Single- and double-quote allow cases are important: they distinguish a real subshell construct from one that's just text inside a string, which is what `extractSubshellGroups` / `extractCommandSubstitutions` quote-awareness is for. --- tests/hooks/pre-bash-dev-server-block.test.js | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/hooks/pre-bash-dev-server-block.test.js b/tests/hooks/pre-bash-dev-server-block.test.js index 7ec978dd..c8409f0b 100644 --- a/tests/hooks/pre-bash-dev-server-block.test.js +++ b/tests/hooks/pre-bash-dev-server-block.test.js @@ -89,6 +89,61 @@ function runTests() { assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); }) ? passed++ : failed++); + // --- Subshell bypass regression (issue: dev server slipped past via $(), ``, ()) --- + + if (!isWindows) { + (test('blocks $(npm run dev) — command substitution', () => { + const result = runScript('$(npm run dev)'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + assert.ok(result.stderr.includes('BLOCKED'), 'expected BLOCKED in stderr'); + }) ? passed++ : failed++); + + (test('blocks `npm run dev` — backtick substitution', () => { + const result = runScript('`npm run dev`'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + }) ? passed++ : failed++); + + (test('blocks echo $(npm run dev) — substitution nested in argument', () => { + const result = runScript('echo $(npm run dev)'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + }) ? passed++ : failed++); + + (test('blocks (npm run dev) — plain subshell group', () => { + const result = runScript('(npm run dev)'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + }) ? passed++ : failed++); + + (test('blocks $(echo a; npm run dev) — substitution with sequenced segments', () => { + const result = runScript('$(echo a; npm run dev)'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + }) ? passed++ : failed++); + + (test('blocks (pnpm dev) — plain subshell group with pnpm', () => { + const result = runScript('(pnpm dev)'); + assert.strictEqual(result.code, 2, `Expected exit code 2, got ${result.code}`); + }) ? passed++ : failed++); + + (test('allows tmux launcher inside subshell wrapping (exit code 0)', () => { + const result = runScript('(tmux new-session -d -s dev "npm run dev")'); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + }) ? passed++ : failed++); + + (test('allows single-quoted "(npm run dev)" — literal string, not a subshell', () => { + const result = runScript("git commit -m '(npm run dev)'"); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + }) ? passed++ : failed++); + + (test('allows double-quoted "(npm run dev)" — literal in double quotes (bash does not subshell)', () => { + const result = runScript('echo "(npm run dev)"'); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + }) ? passed++ : failed++); + + (test("allows single-quoted '$(npm run dev)' — literal string, no substitution", () => { + const result = runScript("git commit -m '$(npm run dev) fix'"); + assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`); + }) ? passed++ : failed++); + } + // --- Edge cases --- (test('empty/invalid input passes through (exit code 0)', () => {