/** * Direct subprocess tests for scripts/hooks/plugin-hook-bootstrap.js. */ 'use strict'; const assert = require('assert'); const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const SCRIPT = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'plugin-hook-bootstrap.js'); function createTempDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-hook-bootstrap-')); } function cleanup(dirPath) { fs.rmSync(dirPath, { recursive: true, force: true }); } function writeFile(root, relativePath, content) { const filePath = path.join(root, relativePath); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, content, 'utf8'); return filePath; } function run(args = [], options = {}) { return spawnSync(process.execPath, [SCRIPT, ...args], { input: options.input || '', encoding: 'utf8', env: { ...process.env, CLAUDE_PLUGIN_ROOT: options.root || '', ECC_PLUGIN_ROOT: options.eccRoot || '', ...(options.env || {}), }, cwd: options.cwd || process.cwd(), timeout: 10000, }); } function test(name, fn) { try { fn(); console.log(` PASS ${name}`); return true; } catch (error) { console.log(` FAIL ${name}`); console.log(` Error: ${error.message}`); return false; } } function runTests() { console.log('\n=== Testing plugin-hook-bootstrap.js ===\n'); let passed = 0; let failed = 0; if (test('passes stdin through when required bootstrap inputs are missing', () => { const result = run([], { input: '{"ok":true}' }); assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout, '{"ok":true}'); assert.strictEqual(result.stderr, ''); })) passed++; else failed++; if (test('node mode runs target script with plugin root environment', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'hook.js'), ` const fs = require('fs'); const raw = fs.readFileSync(0, 'utf8'); process.stdout.write(JSON.stringify({ raw, args: process.argv.slice(2), claudeRoot: process.env.CLAUDE_PLUGIN_ROOT, eccRoot: process.env.ECC_PLUGIN_ROOT, })); `); const result = run(['node', path.join('scripts', 'hook.js'), 'one', 'two'], { root, input: 'payload', }); const parsed = JSON.parse(result.stdout); assert.strictEqual(result.status, 0, result.stderr); assert.strictEqual(parsed.raw, 'payload'); assert.deepStrictEqual(parsed.args, ['one', 'two']); assert.strictEqual(parsed.claudeRoot, root); assert.strictEqual(parsed.eccRoot, root); } finally { cleanup(root); } })) passed++; else failed++; if (test('node mode passes original stdin when child exits cleanly without stdout', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'silent.js'), 'process.exit(0);\n'); const result = run(['node', path.join('scripts', 'silent.js')], { root, input: 'raw-input', }); assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout, 'raw-input'); } finally { cleanup(root); } })) passed++; else failed++; if (test('node mode forwards child stdout and exit status for blocking hooks', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'block.js'), ` process.stdout.write('blocked output'); process.stderr.write('blocked stderr\\n'); process.exit(2); `); const result = run(['node', path.join('scripts', 'block.js')], { root, input: 'raw-input', }); assert.strictEqual(result.status, 2); assert.strictEqual(result.stdout, 'blocked output'); assert.strictEqual(result.stderr, 'blocked stderr\n'); } finally { cleanup(root); } })) passed++; else failed++; if (test('node mode leaves stdout empty for nonzero child without stdout', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'fail.js'), ` process.stderr.write('failure stderr\\n'); process.exit(7); `); const result = run(['node', path.join('scripts', 'fail.js')], { root, input: 'raw-input', }); assert.strictEqual(result.status, 7); assert.strictEqual(result.stdout, ''); assert.strictEqual(result.stderr, 'failure stderr\n'); } finally { cleanup(root); } })) passed++; else failed++; if (test('shell mode runs target script through an available shell', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'hook.sh'), [ 'input=$(cat)', 'printf "shell:%s:%s" "$1" "$input"', '', ].join('\n')); const result = run(['shell', path.join('scripts', 'hook.sh'), 'arg'], { root, input: 'payload', env: fs.existsSync('/bin/sh') ? { BASH: '/bin/sh' } : {}, }); assert.strictEqual(result.status, 0, result.stderr); assert.strictEqual(result.stdout, 'shell:arg:payload'); } finally { cleanup(root); } })) passed++; else failed++; if (test('shell mode fails open when no shell runtime is available', () => { const root = createTempDir(); try { writeFile(root, path.join('scripts', 'hook.sh'), 'printf unreachable\n'); const result = run(['shell', path.join('scripts', 'hook.sh')], { root, input: 'raw-input', env: { PATH: '', BASH: '' }, }); assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout, 'raw-input'); assert.ok(result.stderr.includes('shell runtime unavailable')); } finally { cleanup(root); } })) passed++; else failed++; if (test('rejects target paths that escape the plugin root', () => { const root = createTempDir(); try { const result = run(['node', path.join('..', 'outside.js')], { root, input: 'raw-input', }); assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout, 'raw-input'); assert.ok(result.stderr.includes('Path traversal rejected')); } finally { cleanup(root); } })) passed++; else failed++; if (test('unknown mode fails open with stderr warning', () => { const root = createTempDir(); try { const result = run(['python', 'hook.py'], { root, input: 'raw-input', }); assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout, 'raw-input'); assert.ok(result.stderr.includes('unknown bootstrap mode: python')); } finally { cleanup(root); } })) passed++; else failed++; if (test('missing node target returns child failure diagnostics', () => { const root = createTempDir(); try { const result = run(['node', path.join('scripts', 'missing.js')], { root, input: 'raw-input', }); assert.strictEqual(result.status, 1); assert.strictEqual(result.stdout, ''); assert.ok(result.stderr.includes('Cannot find module')); } finally { cleanup(root); } })) passed++; else failed++; console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } runTests();