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)', () => {