Files
juliusbrussee__caveman/tests/test_mode_tracker_stdout.js
Amir Fathi 9acc70a1c3 fix(hooks): handle stdout/stderr 'error' in UserPromptSubmit hook
caveman-mode-tracker.js never installed an 'error' listener on
process.stdout/process.stderr. When the harness closes its end of the
pipe after the hook has already begun writing a payload (the per-turn
reinforcement or the /caveman-stats block), the write throws EPIPE as
an uncaught exception and the hook exits non-zero, surfacing as a red
"Failed to write to socket (Broken pipe, errno 32)" status line under
an otherwise normal prompt.

This mirrors #538, which added the equivalent listener on stdin for
the same reason: hooks must always exit 0, and an unlistened stream
'error' throws regardless of which side of the pipe closed.

Fixes #397
2026-08-23 10:12:39 +00:00

65 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
// Tests for the stdout/stderr 'error' handlers in caveman-mode-tracker.js.
// Covers issue #397: an abnormal stdout/stderr close (broken pipe, the
// harness tearing down its read end) emits an 'error' event on the stream;
// without a listener Node throws it as an uncaught exception and the hook
// exits non-zero — the same failure shape #538 fixed on the stdin side.
//
// Run: node tests/test_mode_tracker_stdout.js
const path = require('path');
const assert = require('assert');
const { spawnSync } = require('child_process');
const HOOK_PATH = path.resolve(__dirname, '..', 'src', 'hooks', 'caveman-mode-tracker.js');
const CLEAN_EXIT = 0;
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
passed++;
console.log(` ✓ ${name}`);
} catch (e) {
failed++;
console.error(` ✗ ${name}`);
console.error(` ${e.message}`);
}
}
console.log('caveman-mode-tracker stdout/stderr error handling\n');
// Load the REAL hook in a child, then emit an 'error' on the given stream to
// simulate a broken pipe. stdin is left open so the injected 'error' is the
// only event that fires, isolating the handler under test.
function runWithStreamError(streamName) {
const harness =
`require(${JSON.stringify(HOOK_PATH)});` +
`setImmediate(() => process.${streamName}.emit('error', new Error('EPIPE (simulated)')));`;
return spawnSync(process.execPath, ['-e', harness], {
stdio: ['pipe', 'ignore', 'pipe'],
encoding: 'utf8',
});
}
for (const streamName of ['stdout', 'stderr']) {
test(`${streamName} "error" event does not crash the hook (exit 0)`, () => {
const res = runWithStreamError(streamName);
assert.strictEqual(
res.status,
CLEAN_EXIT,
`expected clean exit on ${streamName} error, got status=${res.status} signal=${res.signal}\n` +
`stderr: ${(res.stderr || '').trim()}`
);
assert.ok(
!/Unhandled 'error' event/.test(res.stderr || ''),
`hook leaked an uncaught ${streamName} error:\n${(res.stderr || '').trim()}`
);
});
}
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed === 0 ? 0 : 1);