// hook-helper.mjs — Shared test helper for hook scripts. // Spawns a hook as a child process and feeds it JSON via stdin. import { execFile } from 'node:child_process'; import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; /** * Run a hook script by spawning `node ` and piping `input` to stdin. * * @param {string} scriptPath - Absolute path to the hook .mjs file * @param {object|string} input - JSON payload (object will be stringified) * @returns {Promise<{ code: number, stdout: string, stderr: string }>} */ export function runHook(scriptPath, input) { return runHookWithEnv(scriptPath, input, {}); } /** * Run a hook script with custom environment variables. * * @param {string} scriptPath - Absolute path to the hook .mjs file * @param {object|string} input - JSON payload (object will be stringified) * @param {Record} envOverrides - Extra env vars to set * @returns {Promise<{ code: number, stdout: string, stderr: string }>} */ export function runHookWithEnv(scriptPath, input, envOverrides) { return new Promise((resolve) => { const env = { ...process.env, ...envOverrides }; const child = execFile( 'node', [scriptPath], { timeout: 5000, env }, (err, stdout, stderr) => { resolve({ code: child.exitCode ?? (err && err.code === 'ERR_CHILD_PROCESS_STDIO_FINAL' ? 0 : 1), stdout: stdout || '', stderr: stderr || '', }); } ); child.stdin.end(typeof input === 'string' ? input : JSON.stringify(input)); }); } /** * Run a hook script against a throwaway project root carrying a * `.llm-security/policy.json`. * * v8.0.0 replaced the `LLM_SECURITY_*` mode env-vars with policy.json keys, so * a test that wants non-default hook behaviour has to give the hook a project * root to read. The temp root is removed even when the hook throws. * * @param {string} scriptPath - Absolute path to the hook .mjs file * @param {object|string} input - JSON payload (object will be stringified) * @param {object} policy - Written verbatim to `.llm-security/policy.json` * @param {Record} [envOverrides] - Extra env vars to set * @returns {Promise<{ code: number, stdout: string, stderr: string }>} */ export async function runHookWithPolicy(scriptPath, input, policy, envOverrides = {}) { const root = mkdtempSync(join(tmpdir(), 'llmsec-policy-')); try { mkdirSync(join(root, '.llm-security'), { recursive: true }); writeFileSync(join(root, '.llm-security', 'policy.json'), JSON.stringify(policy)); return await runHookWithEnv(scriptPath, input, { CLAUDE_PROJECT_ROOT: root, ...envOverrides, }); } finally { rmSync(root, { recursive: true, force: true }); } }