Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/providers/src/claude/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,35 @@ describe('ClaudeProvider', () => {
expect(callArgs.options.sandbox).toEqual(sandbox);
});

test('passes hooks to SDK via nodeConfig without crashing on warning extraction', async () => {
// Regression for a TypeError that surfaced whenever a workflow node declared
// `hooks:` in its nodeConfig: applyNodeConfig ran twice — once against a
// throwaway Options `{}` for warning extraction, and once against the real
// Options — and the first pass crashed because it wrote into an undefined
// `options.hooks` map.
mockQuery.mockImplementation(async function* () {
yield { type: 'result', session_id: 'sid' };
});

for await (const _ of client.sendQuery('test', '/tmp', undefined, {
nodeConfig: {
hooks: {
PreToolUse: [{ matcher: 'Write', response: { decision: 'approve' } }],
},
},
})) {
// consume
}

expect(mockQuery).toHaveBeenCalledTimes(1);
const callArgs = mockQuery.mock.calls[0][0] as {
options: { hooks?: Record<string, Array<{ matcher?: string }>> };
};
// Node hooks land alongside the provider's own PostToolUse capture hook.
expect(callArgs.options.hooks?.PreToolUse?.[0]?.matcher).toBe('Write');
expect(callArgs.options.hooks?.PostToolUse).toBeDefined();
});

test('ignores empty text blocks', async () => {
mockQuery.mockImplementation(async function* () {
yield {
Expand Down
6 changes: 6 additions & 0 deletions packages/providers/src/claude/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,12 @@ async function applyNodeConfig(
if (Object.keys(builtHooks).length > 0) {
// Merge with existing hooks (PostToolUse capture hook)
const existingHooks = options.hooks as SDKHooksMap | undefined;
// sendQuery's warning-extraction path passes `{} as Options` (no `hooks`
// field), so direct property assignment below would crash with
// "undefined is not an object". Ensure the map exists before writing.
if (!options.hooks) {
options.hooks = {} as SDKHooksMap;
}
for (const [event, matchers] of Object.entries(builtHooks)) {
if (!matchers) continue;
const existing = existingHooks?.[event] as HookCallbackMatcher[] | undefined;
Expand Down