diff --git a/packages/core/src/hooks/hookAggregator.test.ts b/packages/core/src/hooks/hookAggregator.test.ts index e24bb5e19..07be6178c 100644 --- a/packages/core/src/hooks/hookAggregator.test.ts +++ b/packages/core/src/hooks/hookAggregator.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookAggregator.ts b/packages/core/src/hooks/hookAggregator.ts index 95b901fbc..59c299045 100644 --- a/packages/core/src/hooks/hookAggregator.ts +++ b/packages/core/src/hooks/hookAggregator.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookEventHandler.test.ts b/packages/core/src/hooks/hookEventHandler.test.ts index 0b032e547..48f3bd5d6 100644 --- a/packages/core/src/hooks/hookEventHandler.test.ts +++ b/packages/core/src/hooks/hookEventHandler.test.ts @@ -6,15 +6,7 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { HookEventHandler } from './hookEventHandler.js'; -import { - HookEventName, - HookType, - HooksConfigSource, - NotificationType, - SessionStartSource, - SessionEndReason, - PreCompactTrigger, -} from './types.js'; +import { HookEventName, HookType, HooksConfigSource } from './types.js'; import type { Config } from '../config/config.js'; import type { HookPlanner, @@ -22,7 +14,7 @@ import type { HookAggregator, AggregatedHookResult, } from './index.js'; -import type { HookConfig, HookExecutionResult, HookOutput } from './types.js'; +import type { HookConfig, HookOutput } from './types.js'; describe('HookEventHandler', () => { let mockConfig: Config; @@ -68,17 +60,6 @@ describe('HookEventHandler', () => { eventName: HookEventName.PreToolUse, }); - const createMockExecutionResult = ( - success: boolean = true, - output?: HookOutput, - ): HookExecutionResult => ({ - hookConfig: { type: HookType.Command, command: 'echo test' }, - eventName: HookEventName.PreToolUse, - success, - output, - duration: 100, - }); - const createMockAggregatedResult = ( success: boolean = true, finalOutput?: HookOutput, @@ -90,174 +71,6 @@ describe('HookEventHandler', () => { finalOutput, }); - describe('firePreToolUseEvent', () => { - it('should execute hooks for PreToolUse event', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - const mockResults = [ - createMockExecutionResult(true, { decision: 'allow' }), - ]; - const mockAggregated = createMockAggregatedResult(true, { - decision: 'allow', - }); - - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue( - mockResults, - ); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, - ); - - const result = await hookEventHandler.firePreToolUseEvent('Read', { - path: '/test/file.txt', - }); - - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.PreToolUse, - { toolName: 'Read' }, - ); - expect(mockHookRunner.executeHooksParallel).toHaveBeenCalled(); - expect(result.success).toBe(true); - }); - - it('should include tool name and input in the hook input', async () => { - // Need to provide at least one hook config so the runner is called - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.firePreToolUseEvent('Edit', { file: '/test.txt' }); - - // Verify the mock was called - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalled(); - expect(mockHookRunner.executeHooksParallel).toHaveBeenCalled(); - - // Get the input parameter (3rd argument, index 2) - const inputArg = (mockHookRunner.executeHooksParallel as Mock).mock - .calls[0][2]; - expect(inputArg.tool_name).toBe('Edit'); - expect(inputArg.tool_input).toEqual({ file: '/test.txt' }); - }); - - it('should include mcp_context when provided', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - const mcpContext = { - server_name: 'test-server', - tool_name: 'mcp-tool', - command: 'npx', - }; - - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.firePreToolUseEvent('Bash', {}, mcpContext); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { mcp_context?: typeof mcpContext }; - expect(input.mcp_context).toEqual(mcpContext); - }); - - it('should return empty result when no hooks are configured', async () => { - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(null); - - const result = await hookEventHandler.firePreToolUseEvent('Read', {}); - - expect(result.success).toBe(true); - expect(result.allOutputs).toEqual([]); - }); - }); - - describe('firePostToolUseEvent', () => { - it('should execute hooks for PostToolUse event', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - const mockResults = [ - createMockExecutionResult(true, { decision: 'allow' }), - ]; - const mockAggregated = createMockAggregatedResult(true, { - decision: 'allow', - }); - - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue( - mockResults, - ); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, - ); - - const result = await hookEventHandler.firePostToolUseEvent( - 'Read', - { path: '/test/file.txt' }, - { content: 'file content' }, - ); - - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.PostToolUse, - { toolName: 'Read' }, - ); - expect(result.success).toBe(true); - }); - - it('should include tool_response in the hook input', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.firePostToolUseEvent( - 'Read', - { path: '/test.txt' }, - { content: 'hello' }, - ); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { - tool_response: Record; - }; - expect(input.tool_response).toEqual({ content: 'hello' }); - }); - }); - describe('fireUserPromptSubmitEvent', () => { it('should execute hooks for UserPromptSubmit event', async () => { const mockPlan = createMockExecutionPlan([]); @@ -302,31 +115,6 @@ describe('HookEventHandler', () => { }); }); - describe('fireNotificationEvent', () => { - it('should execute hooks for Notification event', async () => { - const mockPlan = createMockExecutionPlan([]); - const mockAggregated = createMockAggregatedResult(true); - - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, - ); - - const result = await hookEventHandler.fireNotificationEvent( - NotificationType.ToolPermission, - 'Test message', - { key: 'value' }, - ); - - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.Notification, - undefined, - ); - expect(result.success).toBe(true); - }); - }); - describe('fireStopEvent', () => { it('should execute hooks for Stop event', async () => { const mockPlan = createMockExecutionPlan([]); @@ -372,175 +160,35 @@ describe('HookEventHandler', () => { expect(input.stop_hook_active).toBe(true); expect(input.last_assistant_message).toBe('last assistant message'); }); - }); - describe('fireSessionStartEvent', () => { - it('should execute hooks for SessionStart event', async () => { + it('should handle continue=false in final output', async () => { const mockPlan = createMockExecutionPlan([]); - const mockAggregated = createMockAggregatedResult(true); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, + createMockAggregatedResult(true, { + continue: false, + stopReason: 'test stop', + }), ); - const result = await hookEventHandler.fireSessionStartEvent( - SessionStartSource.Startup, - ); + await hookEventHandler.fireStopEvent(); - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.SessionStart, - { trigger: SessionStartSource.Startup }, - ); - expect(result.success).toBe(true); + expect(true).toBe(true); }); - it('should include source in hook input', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.fireSessionStartEvent(SessionStartSource.Resume); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { source: string }; - expect(input.source).toBe(SessionStartSource.Resume); - }); - }); - - describe('fireSessionEndEvent', () => { - it('should execute hooks for SessionEnd event', async () => { + it('should handle missing finalOutput gracefully', async () => { const mockPlan = createMockExecutionPlan([]); - const mockAggregated = createMockAggregatedResult(true); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, + createMockAggregatedResult(true, undefined), ); - const result = await hookEventHandler.fireSessionEndEvent( - SessionEndReason.Clear, - ); + const result = await hookEventHandler.fireStopEvent(); - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.SessionEnd, - { trigger: SessionEndReason.Clear }, - ); expect(result.success).toBe(true); - }); - - it('should include reason in hook input', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.fireSessionEndEvent(SessionEndReason.Logout); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { reason: string }; - expect(input.reason).toBe(SessionEndReason.Logout); - }); - }); - - describe('firePreCompactEvent', () => { - it('should execute hooks for PreCompact event', async () => { - const mockPlan = createMockExecutionPlan([]); - const mockAggregated = createMockAggregatedResult(true); - - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - mockAggregated, - ); - - const result = await hookEventHandler.firePreCompactEvent( - PreCompactTrigger.Manual, - ); - - expect(mockHookPlanner.createExecutionPlan).toHaveBeenCalledWith( - HookEventName.PreCompact, - { trigger: PreCompactTrigger.Manual }, - ); - expect(result.success).toBe(true); - }); - - it('should include trigger in hook input', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.firePreCompactEvent(PreCompactTrigger.Auto); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { trigger: string }; - expect(input.trigger).toBe(PreCompactTrigger.Auto); - }); - }); - - describe('base input creation', () => { - it('should include common fields in all hook inputs', async () => { - const mockPlan = createMockExecutionPlan([ - { - type: HookType.Command, - command: 'echo test', - source: HooksConfigSource.Project, - }, - ]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true), - ); - - await hookEventHandler.firePreToolUseEvent('Read', {}); - - const mockCalls = (mockHookRunner.executeHooksParallel as Mock).mock - .calls; - const input = mockCalls[0][2] as { - session_id: string; - transcript_path: string; - cwd: string; - hook_event_name: string; - timestamp: string; - }; - - expect(input.session_id).toBe('test-session-id'); - expect(input.transcript_path).toBe('/test/transcript'); - expect(input.cwd).toBe('/test/cwd'); - expect(input.hook_event_name).toBe(HookEventName.PreToolUse); - expect(input.timestamp).toBeDefined(); + expect(result.finalOutput).toBeUndefined(); }); }); @@ -563,7 +211,7 @@ describe('HookEventHandler', () => { createMockAggregatedResult(true), ); - await hookEventHandler.firePreToolUseEvent('Read', {}); + await hookEventHandler.fireUserPromptSubmitEvent('test'); expect(mockHookRunner.executeHooksSequential).toHaveBeenCalled(); expect(mockHookRunner.executeHooksParallel).not.toHaveBeenCalled(); @@ -587,7 +235,7 @@ describe('HookEventHandler', () => { createMockAggregatedResult(true), ); - await hookEventHandler.firePreToolUseEvent('Read', {}); + await hookEventHandler.fireUserPromptSubmitEvent('test'); expect(mockHookRunner.executeHooksParallel).toHaveBeenCalled(); expect(mockHookRunner.executeHooksSequential).not.toHaveBeenCalled(); @@ -600,7 +248,7 @@ describe('HookEventHandler', () => { throw new Error('Planner error'); }); - const result = await hookEventHandler.firePreToolUseEvent('Read', {}); + const result = await hookEventHandler.fireUserPromptSubmitEvent('test'); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); @@ -620,74 +268,11 @@ describe('HookEventHandler', () => { new Error('Runner error'), ); - const result = await hookEventHandler.firePreToolUseEvent('Read', {}); + const result = await hookEventHandler.fireUserPromptSubmitEvent('test'); expect(result.success).toBe(false); expect(result.errors).toHaveLength(1); expect(result.errors[0].message).toBe('Runner error'); }); }); - - describe('processCommonHookOutputFields', () => { - it('should handle systemMessage in final output', async () => { - const mockPlan = createMockExecutionPlan([]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true, { - systemMessage: 'test system message', - }), - ); - - await hookEventHandler.firePreToolUseEvent('Read', {}); - - // The method processes the output - we just verify it doesn't throw - expect(true).toBe(true); - }); - - it('should handle continue=false in final output', async () => { - const mockPlan = createMockExecutionPlan([]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true, { - continue: false, - stopReason: 'test stop', - }), - ); - - await hookEventHandler.fireStopEvent(); - - // The method processes the output - we just verify it doesn't throw - expect(true).toBe(true); - }); - - it('should handle suppressOutput in final output', async () => { - const mockPlan = createMockExecutionPlan([]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true, { suppressOutput: true }), - ); - - await hookEventHandler.firePreToolUseEvent('Read', {}); - - // The method processes the output - we just verify it doesn't throw - expect(true).toBe(true); - }); - - it('should handle missing finalOutput gracefully', async () => { - const mockPlan = createMockExecutionPlan([]); - vi.mocked(mockHookPlanner.createExecutionPlan).mockReturnValue(mockPlan); - vi.mocked(mockHookRunner.executeHooksParallel).mockResolvedValue([]); - vi.mocked(mockHookAggregator.aggregateResults).mockReturnValue( - createMockAggregatedResult(true, undefined), - ); - - const result = await hookEventHandler.firePreToolUseEvent('Read', {}); - - expect(result.success).toBe(true); - expect(result.finalOutput).toBeUndefined(); - }); - }); }); diff --git a/packages/core/src/hooks/hookEventHandler.ts b/packages/core/src/hooks/hookEventHandler.ts index b29d9f0aa..2fd5f2892 100644 --- a/packages/core/src/hooks/hookEventHandler.ts +++ b/packages/core/src/hooks/hookEventHandler.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ @@ -13,19 +13,8 @@ import type { HookConfig, HookInput, HookExecutionResult, - PreToolUseInput, - PostToolUseInput, UserPromptSubmitInput, - NotificationInput, StopInput, - SessionStartInput, - SessionEndInput, - PreCompactInput, - NotificationType, - SessionStartSource, - SessionEndReason, - PreCompactTrigger, - McpToolContext, } from './types.js'; import { createDebugLogger } from '../utils/debugLogger.js'; @@ -52,48 +41,6 @@ export class HookEventHandler { this.hookAggregator = hookAggregator; } - /** - * Fire a PreToolUse event - * Called by handleHookExecutionRequest - executes hooks directly - */ - async firePreToolUseEvent( - toolName: string, - toolInput: Record, - mcpContext?: McpToolContext, - ): Promise { - const input: PreToolUseInput = { - ...this.createBaseInput(HookEventName.PreToolUse), - tool_name: toolName, - tool_input: toolInput, - ...(mcpContext && { mcp_context: mcpContext }), - }; - - const context: HookEventContext = { toolName }; - return this.executeHooks(HookEventName.PreToolUse, input, context); - } - - /** - * Fire a PostToolUse event - * Called by handleHookExecutionRequest - executes hooks directly - */ - async firePostToolUseEvent( - toolName: string, - toolInput: Record, - toolResponse: Record, - mcpContext?: McpToolContext, - ): Promise { - const input: PostToolUseInput = { - ...this.createBaseInput(HookEventName.PostToolUse), - tool_name: toolName, - tool_input: toolInput, - tool_response: toolResponse, - ...(mcpContext && { mcp_context: mcpContext }), - }; - - const context: HookEventContext = { toolName }; - return this.executeHooks(HookEventName.PostToolUse, input, context); - } - /** * Fire a UserPromptSubmit event * Called by handleHookExecutionRequest - executes hooks directly @@ -109,24 +56,6 @@ export class HookEventHandler { return this.executeHooks(HookEventName.UserPromptSubmit, input); } - /** - * Fire a Notification event - */ - async fireNotificationEvent( - type: NotificationType, - message: string, - details: Record, - ): Promise { - const input: NotificationInput = { - ...this.createBaseInput(HookEventName.Notification), - notification_type: type, - message, - details, - }; - - return this.executeHooks(HookEventName.Notification, input); - } - /** * Fire a Stop event * Called by handleHookExecutionRequest - executes hooks directly @@ -144,51 +73,6 @@ export class HookEventHandler { return this.executeHooks(HookEventName.Stop, input); } - /** - * Fire a SessionStart event - */ - async fireSessionStartEvent( - source: SessionStartSource, - ): Promise { - const input: SessionStartInput = { - ...this.createBaseInput(HookEventName.SessionStart), - source, - }; - - const context: HookEventContext = { trigger: source }; - return this.executeHooks(HookEventName.SessionStart, input, context); - } - - /** - * Fire a SessionEnd event - */ - async fireSessionEndEvent( - reason: SessionEndReason, - ): Promise { - const input: SessionEndInput = { - ...this.createBaseInput(HookEventName.SessionEnd), - reason, - }; - - const context: HookEventContext = { trigger: reason }; - return this.executeHooks(HookEventName.SessionEnd, input, context); - } - - /** - * Fire a PreCompact event - */ - async firePreCompactEvent( - trigger: PreCompactTrigger, - ): Promise { - const input: PreCompactInput = { - ...this.createBaseInput(HookEventName.PreCompact), - trigger, - }; - - const context: HookEventContext = { trigger }; - return this.executeHooks(HookEventName.PreCompact, input, context); - } - /** * Execute hooks for a specific event (direct execution without MessageBus) * Used as fallback when MessageBus is not available diff --git a/packages/core/src/hooks/hookPlanner.test.ts b/packages/core/src/hooks/hookPlanner.test.ts index 1396814c7..5ea74810b 100644 --- a/packages/core/src/hooks/hookPlanner.test.ts +++ b/packages/core/src/hooks/hookPlanner.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookPlanner.ts b/packages/core/src/hooks/hookPlanner.ts index f3547017d..6482feeee 100644 --- a/packages/core/src/hooks/hookPlanner.ts +++ b/packages/core/src/hooks/hookPlanner.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookRegistry.test.ts b/packages/core/src/hooks/hookRegistry.test.ts index ddf969528..a9e79f5fa 100644 --- a/packages/core/src/hooks/hookRegistry.test.ts +++ b/packages/core/src/hooks/hookRegistry.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookRegistry.ts b/packages/core/src/hooks/hookRegistry.ts index 7fb93c923..54251c495 100644 --- a/packages/core/src/hooks/hookRegistry.ts +++ b/packages/core/src/hooks/hookRegistry.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookRunner.test.ts b/packages/core/src/hooks/hookRunner.test.ts index ddbc87e87..73c1cf665 100644 --- a/packages/core/src/hooks/hookRunner.test.ts +++ b/packages/core/src/hooks/hookRunner.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookRunner.ts b/packages/core/src/hooks/hookRunner.ts index 3bc683f63..b8ed322cb 100644 --- a/packages/core/src/hooks/hookRunner.ts +++ b/packages/core/src/hooks/hookRunner.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/hookSystem.test.ts b/packages/core/src/hooks/hookSystem.test.ts index d2558c591..e87722a21 100644 --- a/packages/core/src/hooks/hookSystem.test.ts +++ b/packages/core/src/hooks/hookSystem.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ @@ -11,14 +11,7 @@ import { HookRunner } from './hookRunner.js'; import { HookAggregator } from './hookAggregator.js'; import { HookPlanner } from './hookPlanner.js'; import { HookEventHandler } from './hookEventHandler.js'; -import { - SessionStartSource, - SessionEndReason, - PreCompactTrigger, - HookType, - HooksConfigSource, - NotificationType, -} from './types.js'; +import { HookType, HooksConfigSource, HookEventName } from './types.js'; import type { Config } from '../config/config.js'; vi.mock('./hookRegistry.js'); @@ -63,14 +56,8 @@ describe('HookSystem', () => { } as unknown as HookPlanner; mockHookEventHandler = { - fireSessionStartEvent: vi.fn(), - fireSessionEndEvent: vi.fn(), - firePreCompactEvent: vi.fn(), fireUserPromptSubmitEvent: vi.fn(), fireStopEvent: vi.fn(), - firePreToolUseEvent: vi.fn(), - firePostToolUseEvent: vi.fn(), - fireNotificationEvent: vi.fn(), } as unknown as HookEventHandler; vi.mocked(HookRegistry).mockImplementation(() => mockHookRegistry); @@ -145,12 +132,13 @@ describe('HookSystem', () => { it('should return all registered hooks', () => { const mockHooks = [ { - name: 'hook1', config: { type: HookType.Command, command: 'echo test', source: HooksConfigSource.Project, }, + source: HooksConfigSource.Project, + eventName: HookEventName.PreToolUse, enabled: true, }, ]; @@ -163,138 +151,6 @@ describe('HookSystem', () => { }); }); - describe('fireSessionStartEvent', () => { - it('should fire session start event and return output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - finalOutput: { - continue: true, - }, - }; - vi.mocked(mockHookEventHandler.fireSessionStartEvent).mockResolvedValue( - mockResult, - ); - - const result = await hookSystem.fireSessionStartEvent( - SessionStartSource.Startup, - ); - - expect(mockHookEventHandler.fireSessionStartEvent).toHaveBeenCalledWith( - SessionStartSource.Startup, - ); - expect(result).toBeDefined(); - }); - - it('should return undefined when no final output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - finalOutput: undefined, - }; - vi.mocked(mockHookEventHandler.fireSessionStartEvent).mockResolvedValue( - mockResult, - ); - - const result = await hookSystem.fireSessionStartEvent( - SessionStartSource.Resume, - ); - - expect(result).toBeUndefined(); - }); - }); - - describe('fireSessionEndEvent', () => { - it('should fire session end event', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - }; - vi.mocked(mockHookEventHandler.fireSessionEndEvent).mockResolvedValue( - mockResult, - ); - - const result = await hookSystem.fireSessionEndEvent( - SessionEndReason.Clear, - ); - - expect(mockHookEventHandler.fireSessionEndEvent).toHaveBeenCalledWith( - SessionEndReason.Clear, - ); - expect(result).toEqual(mockResult); - }); - }); - - describe('firePreCompactEvent', () => { - it('should fire pre compact event', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - }; - vi.mocked(mockHookEventHandler.firePreCompactEvent).mockResolvedValue( - mockResult, - ); - - const result = await hookSystem.firePreCompactEvent( - PreCompactTrigger.Manual, - ); - - expect(mockHookEventHandler.firePreCompactEvent).toHaveBeenCalledWith( - PreCompactTrigger.Manual, - ); - expect(result).toEqual(mockResult); - }); - }); - - describe('fireUserPromptSubmitEvent', () => { - it('should fire user prompt submit event and return output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 50, - finalOutput: { - continue: true, - }, - }; - vi.mocked( - mockHookEventHandler.fireUserPromptSubmitEvent, - ).mockResolvedValue(mockResult); - - const result = await hookSystem.fireUserPromptSubmitEvent('test prompt'); - - expect( - mockHookEventHandler.fireUserPromptSubmitEvent, - ).toHaveBeenCalledWith('test prompt'); - expect(result).toBeDefined(); - }); - - it('should return undefined when no final output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - finalOutput: undefined, - }; - vi.mocked( - mockHookEventHandler.fireUserPromptSubmitEvent, - ).mockResolvedValue(mockResult); - - const result = await hookSystem.fireUserPromptSubmitEvent('test prompt'); - - expect(result).toBeUndefined(); - }); - }); - describe('fireStopEvent', () => { it('should fire stop event and return output', async () => { const mockResult = { @@ -339,78 +195,6 @@ describe('HookSystem', () => { '', ); }); - }); - - describe('firePreToolUseEvent', () => { - it('should fire pre tool use event and return output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - finalOutput: { - decision: 'allow', - }, - }; - vi.mocked(mockHookEventHandler.firePreToolUseEvent).mockResolvedValue( - mockResult, - ); - - const result = await hookSystem.firePreToolUseEvent('Read', { - path: '/test.txt', - }); - - expect(mockHookEventHandler.firePreToolUseEvent).toHaveBeenCalledWith( - 'Read', - { path: '/test.txt' }, - undefined, - ); - expect(result).toBeDefined(); - }); - - it('should include mcpContext when provided', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - finalOutput: { - decision: 'allow', - }, - }; - const mcpContext = { - server_name: 'test-server', - tool_name: 'mcp-tool', - command: 'npx', - }; - vi.mocked(mockHookEventHandler.firePreToolUseEvent).mockResolvedValue( - mockResult, - ); - - await hookSystem.firePreToolUseEvent( - 'Bash', - { command: 'ls' }, - mcpContext, - ); - - expect(mockHookEventHandler.firePreToolUseEvent).toHaveBeenCalledWith( - 'Bash', - { command: 'ls' }, - mcpContext, - ); - }); - - it('should return undefined when error occurs', async () => { - vi.mocked(mockHookEventHandler.firePreToolUseEvent).mockRejectedValue( - new Error('Hook error'), - ); - - const result = await hookSystem.firePreToolUseEvent('Read', { - path: '/test.txt', - }); - - expect(result).toBeUndefined(); - }); it('should return undefined when no final output', async () => { const mockResult = { @@ -420,209 +204,13 @@ describe('HookSystem', () => { totalDuration: 0, finalOutput: undefined, }; - vi.mocked(mockHookEventHandler.firePreToolUseEvent).mockResolvedValue( + vi.mocked(mockHookEventHandler.fireStopEvent).mockResolvedValue( mockResult, ); - const result = await hookSystem.firePreToolUseEvent('Read', {}); + const result = await hookSystem.fireStopEvent(); expect(result).toBeUndefined(); }); }); - - describe('firePostToolUseEvent', () => { - it('should fire post tool use event and return output', async () => { - const mockResult = { - success: true, - allOutputs: [], - errors: [], - totalDuration: 100, - finalOutput: { - decision: 'allow', - }, - }; - vi.mocked(mockHookEventHandler.firePostToolUseEvent).mockResolvedValue( - mockResult, - ); - - const toolResponse = { - llmContent: 'file content', - returnDisplay: true, - error: null, - }; - - const result = await hookSystem.firePostToolUseEvent( - 'Read', - { path: '/test.txt' }, - toolResponse, - ); - - expect(mockHookEventHandler.firePostToolUseEvent).toHaveBeenCalledWith( - 'Read', - { path: '/test.txt' }, - toolResponse, - undefined, - ); - expect(result).toBeDefined(); - }); - - it('should return undefined when error occurs', async () => { - vi.mocked(mockHookEventHandler.firePostToolUseEvent).mockRejectedValue( - new Error('Hook error'), - ); - - const result = await hookSystem.firePostToolUseEvent( - 'Read', - {}, - { llmContent: null, returnDisplay: false, error: null }, - ); - - expect(result).toBeUndefined(); - }); - }); - - describe('fireToolNotificationEvent', () => { - it('should fire notification event for edit type', async () => { - vi.mocked(mockHookEventHandler.fireNotificationEvent).mockResolvedValue({ - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - }); - - const confirmationDetails = { - type: 'edit' as const, - title: 'Edit File', - fileName: 'test.txt', - filePath: '/test/test.txt', - fileDiff: 'diff', - originalContent: 'old', - newContent: 'new', - isModifying: true, - }; - - await hookSystem.fireToolNotificationEvent(confirmationDetails); - - expect(mockHookEventHandler.fireNotificationEvent).toHaveBeenCalledWith( - NotificationType.ToolPermission, - 'Tool Edit File requires editing', - { - type: 'edit', - title: 'Edit File', - fileName: 'test.txt', - filePath: '/test/test.txt', - fileDiff: 'diff', - originalContent: 'old', - newContent: 'new', - isModifying: true, - }, - ); - }); - - it('should fire notification event for exec type', async () => { - vi.mocked(mockHookEventHandler.fireNotificationEvent).mockResolvedValue({ - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - }); - - const confirmationDetails = { - type: 'exec' as const, - title: 'Run Command', - command: 'ls -la', - rootCommand: 'ls', - }; - - await hookSystem.fireToolNotificationEvent(confirmationDetails); - - expect(mockHookEventHandler.fireNotificationEvent).toHaveBeenCalledWith( - NotificationType.ToolPermission, - 'Tool Run Command requires execution', - { - type: 'exec', - title: 'Run Command', - command: 'ls -la', - rootCommand: 'ls', - }, - ); - }); - - it('should fire notification event for mcp type', async () => { - vi.mocked(mockHookEventHandler.fireNotificationEvent).mockResolvedValue({ - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - }); - - const confirmationDetails = { - type: 'mcp' as const, - title: 'MCP Tool', - serverName: 'test-server', - toolName: 'mcp-tool', - toolDisplayName: 'MCP Tool', - }; - - await hookSystem.fireToolNotificationEvent(confirmationDetails); - - expect(mockHookEventHandler.fireNotificationEvent).toHaveBeenCalledWith( - NotificationType.ToolPermission, - 'Tool MCP Tool requires MCP', - { - type: 'mcp', - title: 'MCP Tool', - serverName: 'test-server', - toolName: 'mcp-tool', - toolDisplayName: 'MCP Tool', - }, - ); - }); - - it('should fire notification event for info type', async () => { - vi.mocked(mockHookEventHandler.fireNotificationEvent).mockResolvedValue({ - success: true, - allOutputs: [], - errors: [], - totalDuration: 0, - }); - - const confirmationDetails = { - type: 'info' as const, - title: 'Info Tool', - prompt: 'Some prompt', - urls: ['https://example.com'], - }; - - await hookSystem.fireToolNotificationEvent(confirmationDetails); - - expect(mockHookEventHandler.fireNotificationEvent).toHaveBeenCalledWith( - NotificationType.ToolPermission, - 'Tool Info Tool requires information', - { - type: 'info', - title: 'Info Tool', - prompt: 'Some prompt', - urls: ['https://example.com'], - }, - ); - }); - - it('should handle error gracefully', async () => { - vi.mocked(mockHookEventHandler.fireNotificationEvent).mockRejectedValue( - new Error('Notification error'), - ); - - const confirmationDetails = { - type: 'info' as const, - title: 'Info Tool', - prompt: 'Some prompt', - urls: [], - }; - - await expect( - hookSystem.fireToolNotificationEvent(confirmationDetails), - ).resolves.not.toThrow(); - }); - }); }); diff --git a/packages/core/src/hooks/hookSystem.ts b/packages/core/src/hooks/hookSystem.ts index 22d5dfa58..8a40cbd9e 100644 --- a/packages/core/src/hooks/hookSystem.ts +++ b/packages/core/src/hooks/hookSystem.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ @@ -12,16 +12,8 @@ import { HookPlanner } from './hookPlanner.js'; import { HookEventHandler } from './hookEventHandler.js'; import type { HookRegistryEntry } from './hookRegistry.js'; import { createDebugLogger } from '../utils/debugLogger.js'; -import type { - SessionStartSource, - SessionEndReason, - PreCompactTrigger, - DefaultHookOutput, - McpToolContext, -} from './types.js'; -import { NotificationType, createHookOutput } from './types.js'; -import type { AggregatedHookResult } from './hookAggregator.js'; -import type { ToolCallConfirmationDetails } from '../tools/tools.js'; +import type { DefaultHookOutput } from './types.js'; +import { createHookOutput } from './types.js'; const debugLogger = createDebugLogger('TRUSTED_HOOKS'); @@ -29,73 +21,6 @@ const debugLogger = createDebugLogger('TRUSTED_HOOKS'); * Main hook system that coordinates all hook-related functionality */ -/** - * Converts ToolCallConfirmationDetails to a serializable format for hooks. - * Excludes function properties (onConfirm, ideConfirmation) that can't be serialized. - */ -function toSerializableDetails( - details: ToolCallConfirmationDetails, -): Record { - const base: Record = { - type: details.type, - title: details.title, - }; - - switch (details.type) { - case 'edit': - return { - ...base, - fileName: details.fileName, - filePath: details.filePath, - fileDiff: details.fileDiff, - originalContent: details.originalContent, - newContent: details.newContent, - isModifying: details.isModifying, - }; - case 'exec': - return { - ...base, - command: details.command, - rootCommand: details.rootCommand, - }; - case 'mcp': - return { - ...base, - serverName: details.serverName, - toolName: details.toolName, - toolDisplayName: details.toolDisplayName, - }; - case 'info': - return { - ...base, - prompt: details.prompt, - urls: details.urls, - }; - default: - return base; - } -} - -/** - * Gets the message to display in the notification hook for tool confirmation. - */ -function getNotificationMessage( - confirmationDetails: ToolCallConfirmationDetails, -): string { - switch (confirmationDetails.type) { - case 'edit': - return `Tool ${confirmationDetails.title} requires editing`; - case 'exec': - return `Tool ${confirmationDetails.title} requires execution`; - case 'mcp': - return `Tool ${confirmationDetails.title} requires MCP`; - case 'info': - return `Tool ${confirmationDetails.title} requires information`; - default: - return `Tool requires confirmation`; - } -} - export class HookSystem { private readonly hookRegistry: HookRegistry; private readonly hookRunner: HookRunner; @@ -153,30 +78,6 @@ export class HookSystem { return this.hookRegistry.getAllHooks(); } - /** - * Fire hook events directly - */ - async fireSessionStartEvent( - source: SessionStartSource, - ): Promise { - const result = await this.hookEventHandler.fireSessionStartEvent(source); - return result.finalOutput - ? createHookOutput('SessionStart', result.finalOutput) - : undefined; - } - - async fireSessionEndEvent( - reason: SessionEndReason, - ): Promise { - return this.hookEventHandler.fireSessionEndEvent(reason); - } - - async firePreCompactEvent( - trigger: PreCompactTrigger, - ): Promise { - return this.hookEventHandler.firePreCompactEvent(trigger); - } - async fireUserPromptSubmitEvent( prompt: string, ): Promise { @@ -199,70 +100,4 @@ export class HookSystem { ? createHookOutput('Stop', result.finalOutput) : undefined; } - - async firePreToolUseEvent( - toolName: string, - toolInput: Record, - mcpContext?: McpToolContext, - ): Promise { - try { - const result = await this.hookEventHandler.firePreToolUseEvent( - toolName, - toolInput, - mcpContext, - ); - return result.finalOutput - ? createHookOutput('PreToolUse', result.finalOutput) - : undefined; - } catch (error) { - debugLogger.debug(`PreToolUseEvent failed for ${toolName}:`, error); - return undefined; - } - } - - async firePostToolUseEvent( - toolName: string, - toolInput: Record, - toolResponse: { - llmContent: unknown; - returnDisplay: unknown; - error: unknown; - }, - mcpContext?: McpToolContext, - ): Promise { - try { - const result = await this.hookEventHandler.firePostToolUseEvent( - toolName, - toolInput, - toolResponse as Record, - mcpContext, - ); - return result.finalOutput - ? createHookOutput('PostToolUse', result.finalOutput) - : undefined; - } catch (error) { - debugLogger.debug(`PostToolUseEvent failed for ${toolName}:`, error); - return undefined; - } - } - - async fireToolNotificationEvent( - confirmationDetails: ToolCallConfirmationDetails, - ): Promise { - try { - const message = getNotificationMessage(confirmationDetails); - const serializedDetails = toSerializableDetails(confirmationDetails); - - await this.hookEventHandler.fireNotificationEvent( - NotificationType.ToolPermission, - message, - serializedDetails, - ); - } catch (error) { - debugLogger.debug( - `NotificationEvent failed for ${confirmationDetails.title}:`, - error, - ); - } - } } diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index 620130d9f..779f3b332 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/trustedHooks.ts b/packages/core/src/hooks/trustedHooks.ts index 04e93500f..135fcc5b2 100644 --- a/packages/core/src/hooks/trustedHooks.ts +++ b/packages/core/src/hooks/trustedHooks.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */ diff --git a/packages/core/src/hooks/types.ts b/packages/core/src/hooks/types.ts index 66510d86b..49ac7a5ef 100644 --- a/packages/core/src/hooks/types.ts +++ b/packages/core/src/hooks/types.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2026 Qwen + * Copyright 2026 Qwen Team * SPDX-License-Identifier: Apache-2.0 */