Skip to content

Commit b0e78db

Browse files
grypezclaude
andcommitted
test(caprock): integration test for hook binary startup
Spawns dist/bin/hook.mjs as a child process and asserts it exits cleanly without @endo/SES missing-globals errors. Builds the binary in beforeAll so the test is always self-contained and never silently skips due to a stale or missing dist/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d07e5fc commit b0e78db

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

packages/caprock/bin/hook.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* eslint-disable n/no-process-env */
2+
import { execFile, spawn } from 'node:child_process';
3+
import { mkdtemp, rm } from 'node:fs/promises';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { fileURLToPath } from 'node:url';
7+
import { promisify } from 'node:util';
8+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
9+
10+
const execFileAsync = promisify(execFile);
11+
12+
const HOOK_BIN = fileURLToPath(
13+
new URL('../dist/bin/hook.mjs', import.meta.url),
14+
);
15+
const PKG_DIR = fileURLToPath(new URL('..', import.meta.url));
16+
17+
/**
18+
* Spawn hook.mjs with a JSON payload on stdin and collect all output.
19+
*
20+
* @param payload - The hook event payload to send.
21+
* @param env - Extra environment variables.
22+
* @param timeoutMs - Kill timeout in milliseconds.
23+
* @returns stdout, stderr, and exit code.
24+
*/
25+
async function runHook(
26+
payload: unknown,
27+
env: NodeJS.ProcessEnv,
28+
timeoutMs: number,
29+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
30+
return new Promise((resolve, reject) => {
31+
const child = spawn('node', [HOOK_BIN], {
32+
stdio: ['pipe', 'pipe', 'pipe'],
33+
env: { ...process.env, ...env },
34+
});
35+
36+
let stdout = '';
37+
let stderr = '';
38+
39+
child.stdout.on('data', (chunk: Buffer) => {
40+
stdout += chunk.toString();
41+
});
42+
child.stderr.on('data', (chunk: Buffer) => {
43+
stderr += chunk.toString();
44+
});
45+
46+
const timer = setTimeout(() => {
47+
child.kill();
48+
reject(new Error(`Hook timed out after ${timeoutMs}ms`));
49+
}, timeoutMs);
50+
51+
child.on('close', (code) => {
52+
clearTimeout(timer);
53+
resolve({ stdout, stderr, exitCode: code ?? -1 });
54+
});
55+
56+
child.on('error', (error) => {
57+
clearTimeout(timer);
58+
reject(error);
59+
});
60+
61+
child.stdin.write(JSON.stringify(payload));
62+
child.stdin.end();
63+
});
64+
}
65+
66+
describe('hook binary', () => {
67+
let ocapHome: string;
68+
69+
beforeAll(async () => {
70+
await execFileAsync('yarn', ['build'], { cwd: PKG_DIR });
71+
ocapHome = await mkdtemp(join(tmpdir(), 'caprock-hook-test-'));
72+
}, 60_000);
73+
74+
afterAll(async () => {
75+
await rm(ocapHome, { recursive: true, force: true });
76+
});
77+
78+
it('loads without SES globals (SessionStart)', async () => {
79+
const { stderr, exitCode } = await runHook(
80+
{
81+
hook_event_name: 'SessionStart',
82+
session_id: 'hook-integration-test',
83+
transcript_path: '/dev/null',
84+
},
85+
{ OCAP_HOME: ocapHome },
86+
8_000,
87+
);
88+
89+
expect(exitCode).toBe(0);
90+
expect(stderr).not.toMatch(/harden is not defined/u);
91+
expect(stderr).not.toMatch(/Cannot initialize @endo\/errors/u);
92+
expect(stderr).not.toMatch(/missing globalThis\.assert/u);
93+
}, 8_000);
94+
95+
it('loads without SES globals (PreToolUse)', async () => {
96+
const { stdout, stderr, exitCode } = await runHook(
97+
{
98+
hook_event_name: 'PreToolUse',
99+
session_id: 'hook-integration-test',
100+
transcript_path: '/dev/null',
101+
tool_name: 'Bash',
102+
tool_input: { command: 'ls -la' },
103+
},
104+
{ OCAP_HOME: ocapHome },
105+
8_000,
106+
);
107+
108+
expect(exitCode).toBe(0);
109+
expect(stderr).not.toMatch(/harden is not defined/u);
110+
expect(stderr).not.toMatch(/Cannot initialize @endo\/errors/u);
111+
expect(stderr).not.toMatch(/missing globalThis\.assert/u);
112+
// With no daemon running the hook must not block — it passes through.
113+
expect(stdout).toContain('"continue":true');
114+
}, 8_000);
115+
});

0 commit comments

Comments
 (0)