Skip to content

Commit 1953246

Browse files
committed
test: add a claudeHookPayload factory for Claude hook tests
Valid hook events now come from a fishery factory (matching the existing codexStopHookPayload one) instead of seven hand-rolled literals. Malformed payload tests keep inline literals since they intentionally violate the type.
1 parent c5c93c4 commit 1953246

2 files changed

Lines changed: 24 additions & 60 deletions

File tree

packages/cli/src/commands/hookClaude.test.ts

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, it } from 'bun:test';
44
import { CommanderError } from 'commander';
55
import { type RunAnnotationArgs, runAnnotation } from '#src/annotation/runAnnotation.ts';
66
import { claudeHookResponse } from '#src/formatters/plan/claudeHookResponse.ts';
7-
import { annotationArgs } from '#src/testFactories.ts';
7+
import { annotationArgs, claudeHookPayload } from '#src/testFactories.ts';
88
import { createAnnotationDependencies, createStubContext, readErrorLogs } from '#src/testHelpers/index.ts';
99
import { type HookClaudeDependencies, runHookClaude } from './hookClaude.ts';
1010

@@ -13,50 +13,30 @@ describe('hookClaude handler', () => {
1313
const { context, io } = createStubContext();
1414
const submission = annotationSubmission.build({ status: 'approved', threads: [] });
1515
const deps = createHookDependencies({ submission });
16-
const toolInput = { plan: '# Plan\n\nStep 1.\n', planFilePath: '/home/user/.claude/plans/sample.md' };
17-
io.stdin.write(
18-
JSON.stringify({
19-
session_id: 'sess_123',
20-
transcript_path: '/tmp/transcript.json',
21-
cwd: '/work',
22-
permission_mode: 'plan',
23-
hook_event_name: 'PermissionRequest',
24-
tool_name: 'ExitPlanMode',
25-
tool_input: toolInput,
26-
}),
27-
);
16+
const payload = claudeHookPayload.build();
17+
io.stdin.write(JSON.stringify(payload));
2818
io.stdin.end();
2919

3020
await runHookClaude(context, deps);
3121

32-
expect(io.stdout.text()).toBe(`${JSON.stringify(claudeHookResponse(submission, toolInput))}\n`);
22+
expect(io.stdout.text()).toBe(`${JSON.stringify(claudeHookResponse(submission, payload.tool_input))}\n`);
3323
const parsed = JSON.parse(io.stdout.text().trim()) as ReturnType<typeof claudeHookResponse>;
3424
if (parsed.hookSpecificOutput.decision.behavior !== 'allow') throw new Error('expected allow');
35-
expect(parsed.hookSpecificOutput.decision.updatedInput).toEqual(toolInput);
36-
expect(deps.calls).toEqual([annotationArgs.build({ content: '# Plan\n\nStep 1.\n', entrypoint: 'hook_claude' })]);
25+
expect(parsed.hookSpecificOutput.decision.updatedInput).toEqual(payload.tool_input);
26+
expect(deps.calls).toEqual([annotationArgs.build({ content: payload.tool_input.plan, entrypoint: 'hook_claude' })]);
3727
});
3828

3929
it('emits a deny envelope with the markdown feedback when changes are requested', async () => {
4030
const { context, io } = createStubContext();
4131
const submission = annotationSubmission.build();
4232
const deps = createHookDependencies({ submission });
43-
const planContent = '# Plan\n\nStep 1.\n';
44-
io.stdin.write(
45-
JSON.stringify({
46-
session_id: 'sess_123',
47-
transcript_path: '/tmp/transcript.json',
48-
cwd: '/work',
49-
permission_mode: 'plan',
50-
hook_event_name: 'PermissionRequest',
51-
tool_name: 'ExitPlanMode',
52-
tool_input: { plan: planContent },
53-
}),
54-
);
33+
const payload = claudeHookPayload.build();
34+
io.stdin.write(JSON.stringify(payload));
5535
io.stdin.end();
5636

5737
await runHookClaude(context, deps);
5838

59-
const expected = claudeHookResponse(submission, { plan: planContent });
39+
const expected = claudeHookResponse(submission, payload.tool_input);
6040
expect(io.stdout.text()).toBe(`${JSON.stringify(expected)}\n`);
6141
const parsed = JSON.parse(io.stdout.text().trim()) as typeof expected;
6242
expect(parsed.hookSpecificOutput.decision.behavior).toBe('deny');
@@ -70,17 +50,7 @@ describe('hookClaude handler', () => {
7050
const deps: HookClaudeDependencies = {
7151
runReview: (reviewCtx, args) => runAnnotation(reviewCtx, args, createAnnotationDependencies({ submission })),
7252
};
73-
io.stdin.write(
74-
JSON.stringify({
75-
session_id: 'sess_123',
76-
transcript_path: '/tmp/transcript.json',
77-
cwd: '/work',
78-
permission_mode: 'plan',
79-
hook_event_name: 'PermissionRequest',
80-
tool_name: 'ExitPlanMode',
81-
tool_input: { plan: '# Plan\n\nStep 1.\n' },
82-
}),
83-
);
53+
io.stdin.write(JSON.stringify(claudeHookPayload.build()));
8454
io.stdin.end();
8555

8656
await runHookClaude(context, deps);
@@ -99,16 +69,7 @@ describe('hookClaude handler', () => {
9969
it('aborts when hook_event_name is unsupported', () => {
10070
const { context, io, logs } = createStubContext();
10171
const deps = createHookDependencies();
102-
io.stdin.write(
103-
JSON.stringify({
104-
session_id: 'sess_123',
105-
transcript_path: '/tmp/transcript.json',
106-
cwd: '/work',
107-
hook_event_name: 'PreToolUse',
108-
tool_name: 'ExitPlanMode',
109-
tool_input: { plan: '# x' },
110-
}),
111-
);
72+
io.stdin.write(JSON.stringify(claudeHookPayload.build({ hook_event_name: 'PreToolUse' })));
11273
io.stdin.end();
11374

11475
expect(runHookClaude(context, deps)).rejects.toBeInstanceOf(CommanderError);
@@ -120,16 +81,7 @@ describe('hookClaude handler', () => {
12081
it('aborts when PermissionRequest arrives for a tool other than ExitPlanMode', () => {
12182
const { context, io, logs } = createStubContext();
12283
const deps = createHookDependencies();
123-
io.stdin.write(
124-
JSON.stringify({
125-
session_id: 'sess_123',
126-
transcript_path: '/tmp/transcript.json',
127-
cwd: '/work',
128-
hook_event_name: 'PermissionRequest',
129-
tool_name: 'Bash',
130-
tool_input: { plan: 'unused' },
131-
}),
132-
);
84+
io.stdin.write(JSON.stringify(claudeHookPayload.build({ tool_name: 'Bash' })));
13385
io.stdin.end();
13486

13587
expect(runHookClaude(context, deps)).rejects.toBeInstanceOf(CommanderError);

packages/cli/src/testFactories.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import type { HarnessDescriptor } from '@contextbridge/harness';
22
import { Factory } from 'fishery';
33
import type { RunAnnotationArgs } from '#src/annotation/runAnnotation.ts';
4+
import type { ClaudeHookPayload } from '#src/commands/claudeHookSchema.ts';
45
import type {
56
CodexStopHookPayload,
67
CodexTranscriptHookPromptLine,
78
CodexTranscriptPlanLine,
89
} from '#src/commands/codexHookSchema.ts';
910
import type { Environment } from '#src/environment.ts';
11+
import type { ClaudeExitPlanModeInput } from '#src/formatters/plan/claudeHookResponse.ts';
12+
13+
export const claudeHookPayload = Factory.define<ClaudeHookPayload & { tool_input: ClaudeExitPlanModeInput }>(() => ({
14+
session_id: 'sess_123',
15+
transcript_path: '/tmp/transcript.json',
16+
cwd: '/work',
17+
permission_mode: 'plan',
18+
hook_event_name: 'PermissionRequest',
19+
tool_name: 'ExitPlanMode',
20+
tool_input: { plan: '# Plan\n\nStep 1.\n', planFilePath: '/home/user/.claude/plans/sample.md' },
21+
}));
1022

1123
export const codexStopHookPayload = Factory.define<CodexStopHookPayload>(() => ({
1224
session_id: 'sess_123',

0 commit comments

Comments
 (0)