mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-16 20:25:36 +00:00
100 lines
3.7 KiB
TypeScript
100 lines
3.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
const RUNNER_MODULE = '../../src/agent/hooks/runner' as string;
|
|
|
|
interface HookResult {
|
|
action: 'allow' | 'block';
|
|
message?: string;
|
|
reason?: string;
|
|
stdout?: string;
|
|
stderr?: string;
|
|
timedOut?: boolean;
|
|
structuredOutput?: boolean;
|
|
}
|
|
|
|
type RunHook = (
|
|
command: string,
|
|
input: Record<string, unknown>,
|
|
options: { timeout: number; cwd?: string },
|
|
) => Promise<HookResult>;
|
|
|
|
async function importRunHook(): Promise<RunHook> {
|
|
const mod = (await import(RUNNER_MODULE)) as { runHook: RunHook };
|
|
return mod.runHook;
|
|
}
|
|
|
|
describe('runHook process runner', () => {
|
|
it('returns allow when the hook exits 0 and captures stdout', async () => {
|
|
const runHook = await importRunHook();
|
|
const result = await runHook('echo ok', { tool_name: 'Shell' }, { timeout: 5 });
|
|
expect(result.action).toBe('allow');
|
|
expect(result.stdout?.trim()).toBe('ok');
|
|
});
|
|
|
|
it('parses stdout JSON message into a hook result message', async () => {
|
|
const runHook = await importRunHook();
|
|
const result = await runHook('echo \'{"message":"hook says hi"}\'', {}, { timeout: 5 });
|
|
expect(result.action).toBe('allow');
|
|
expect(result.message).toBe('hook says hi');
|
|
expect(result.structuredOutput).toBe(true);
|
|
});
|
|
|
|
it('marks structured stdout JSON without message as empty hook output', async () => {
|
|
const runHook = await importRunHook();
|
|
|
|
const emptyObject = await runHook("echo '{}'", {}, { timeout: 5 });
|
|
expect(emptyObject.action).toBe('allow');
|
|
expect(emptyObject.message).toBeUndefined();
|
|
expect(emptyObject.structuredOutput).toBe(true);
|
|
|
|
const emptyHookSpecificOutput = await runHook(
|
|
'echo \'{"hookSpecificOutput":{}}\'',
|
|
{},
|
|
{ timeout: 5 },
|
|
);
|
|
expect(emptyHookSpecificOutput.action).toBe('allow');
|
|
expect(emptyHookSpecificOutput.message).toBeUndefined();
|
|
expect(emptyHookSpecificOutput.structuredOutput).toBe(true);
|
|
});
|
|
|
|
it('returns block when the hook exits 2 and captures stderr as the reason', async () => {
|
|
const runHook = await importRunHook();
|
|
const result = await runHook(
|
|
"echo 'blocked' >&2; exit 2",
|
|
{ tool_name: 'Shell' },
|
|
{ timeout: 5 },
|
|
);
|
|
expect(result.action).toBe('block');
|
|
expect(result.reason).toContain('blocked');
|
|
});
|
|
|
|
it('returns allow on non-zero, non-2 exit codes (e.g. exit 1)', async () => {
|
|
const runHook = await importRunHook();
|
|
const result = await runHook('exit 1', { tool_name: 'Shell' }, { timeout: 5 });
|
|
expect(result.action).toBe('allow');
|
|
});
|
|
|
|
it('returns allow with timedOut=true when the command exceeds the timeout', async () => {
|
|
const runHook = await importRunHook();
|
|
const result = await runHook('sleep 10', { tool_name: 'Shell' }, { timeout: 1 });
|
|
expect(result.action).toBe('allow');
|
|
expect(result.timedOut).toBe(true);
|
|
});
|
|
|
|
it('parses stdout JSON permissionDecision=deny into a block result with the supplied reason', async () => {
|
|
const runHook = await importRunHook();
|
|
const cmd =
|
|
'echo \'{"hookSpecificOutput": {"permissionDecision": "deny", "permissionDecisionReason": "use rg"}}\'';
|
|
const result = await runHook(cmd, { tool_name: 'Bash' }, { timeout: 5 });
|
|
expect(result.action).toBe('block');
|
|
expect(result.reason).toBe('use rg');
|
|
});
|
|
|
|
it('writes the input payload to the hook process stdin as JSON', async () => {
|
|
const runHook = await importRunHook();
|
|
const cmd =
|
|
'node -e "let s=\\"\\";process.stdin.on(\\"data\\",d=>s+=d);process.stdin.on(\\"end\\",()=>{const o=JSON.parse(s);process.stdout.write(o.tool_name);})"';
|
|
const result = await runHook(cmd, { tool_name: 'WriteFile' }, { timeout: 5 });
|
|
expect(result.stdout?.trim()).toBe('WriteFile');
|
|
});
|
|
});
|