Files
ECC/tests/hooks/hook-input.test.js
T
He DongandGitHub 8bf16ccfec fix(hooks): keep silent hook paths silent (#2987)
* fix(hooks): keep silent hook paths silent

* fix(hooks): harden stream failure handling

* fix(hooks): settle interrupted input streams

* test(hooks): name stream input limits

* test(hooks): isolate PostToolUse dispatcher fixtures
2026-09-18 18:42:20 -04:00

130 lines
3.5 KiB
JavaScript

/**
* Regression tests for bounded hook stdin reads.
*/
'use strict';
const assert = require('assert');
const { PassThrough } = require('stream');
const { readStdinRaw } = require('../../scripts/hooks/hook-input');
const { run: runConfigProtection } = require('../../scripts/hooks/config-protection');
const TEST_STDIN_LIMIT = 1024;
const STREAM_SETTLEMENT_TIMEOUT_MS = 500;
async function test(name, fn) {
try {
await fn();
console.log(` ✓ ${name}`);
return true;
} catch (error) {
console.log(` ✗ ${name}`);
console.log(` Error: ${error.message}`);
return false;
}
}
async function readFromErroredStream(partialInput) {
const stream = new PassThrough();
const resultPromise = readStdinRaw(stream, { maxStdin: TEST_STDIN_LIMIT });
stream.write(partialInput);
stream.destroy(new Error('simulated stdin read failure'));
return resultPromise;
}
async function readFromClosedStream(partialInput) {
const stream = new PassThrough();
const resultPromise = readStdinRaw(stream, { maxStdin: TEST_STDIN_LIMIT });
stream.write(partialInput);
stream.destroy();
return new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error('readStdinRaw did not settle after stream close')),
STREAM_SETTLEMENT_TIMEOUT_MS
);
resultPromise.then(
result => {
clearTimeout(timer);
resolve(result);
},
error => {
clearTimeout(timer);
reject(error);
}
);
});
}
async function runTests() {
console.log('\nHook input reader tests:');
let passed = 0;
let failed = 0;
if (
await test('clean end preserves complete input', async () => {
const stream = new PassThrough();
const resultPromise = readStdinRaw(stream, { maxStdin: TEST_STDIN_LIMIT });
stream.end('{"complete":true}');
assert.deepStrictEqual(await resultPromise, {
raw: '{"complete":true}',
truncated: false
});
})
)
passed++;
else failed++;
if (
await test('stream error marks partial input as truncated', async () => {
const partialInput = '{"tool_name":"Write","tool_input":{';
const result = await readFromErroredStream(partialInput);
assert.strictEqual(result.raw, partialInput);
assert.strictEqual(result.truncated, true);
})
)
passed++;
else failed++;
if (
await test('close without end marks partial input as truncated', async () => {
const partialInput = '{"tool_name":"Write","tool_input":{';
const result = await readFromClosedStream(partialInput);
assert.strictEqual(result.raw, partialInput);
assert.strictEqual(result.truncated, true);
})
)
passed++;
else failed++;
if (
await test('errored partial PreToolUse input remains fail closed', async () => {
const partialInput = '{"tool_name":"Write","tool_input":{"file_path":".eslintrc.js"';
const inputResult = await readFromErroredStream(partialInput);
const hookResult = runConfigProtection(inputResult.raw, {
truncated: inputResult.truncated,
maxStdin: TEST_STDIN_LIMIT
});
assert.strictEqual(inputResult.truncated, true);
assert.strictEqual(hookResult.exitCode, 2);
assert.match(hookResult.stderr, /Refusing to bypass config-protection/);
})
)
passed++;
else failed++;
console.log(`\nPassed: ${passed}`);
console.log(`Failed: ${failed}\n`);
process.exitCode = failed > 0 ? 1 : 0;
}
runTests().catch(error => {
console.error(error);
process.exitCode = 1;
});