import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ToolRouter } from '@/lib/control-center/tool-router'; import { MCPClient } from '@/lib/control-center/mcp-client'; import { appToolsRegistry, appToolDefinitions } from '@/lib/control-center/app-tools'; import { ToolCall, ToolContext, ToolDefinition } from '@/lib/control-center/types'; // Mock the MCP client vi.mock('@/lib/control-center/mcp-client'); // Mock the app tools registry vi.mock('@/lib/control-center/app-tools', () => ({ appToolsRegistry: { getDefinitions: vi.fn(), has: vi.fn(), execute: vi.fn() }, appToolDefinitions: [ { name: 'get_dashboard_stats', description: 'Get dashboard stats', inputSchema: { type: 'object', properties: {} } }, { name: 'list_recent_contacts', description: 'List contacts', inputSchema: { type: 'object', properties: {} } } ] })); describe('ToolRouter', () => { let mockMcpClient: MCPClient; let mockContext: ToolContext; beforeEach(() => { vi.clearAllMocks(); // Create mock MCP client mockMcpClient = { getTools: vi.fn(), executeTool: vi.fn(), healthCheck: vi.fn(), clearCache: vi.fn() } as unknown as MCPClient; // Default context for tests mockContext = { userId: 'user-123', ghlClient: null }; // Setup default app tools mock vi.mocked(appToolsRegistry.getDefinitions).mockReturnValue(appToolDefinitions); vi.mocked(appToolsRegistry.has).mockReturnValue(false); }); describe('getAllTools', () => { it('should return combined MCP and app tools', async () => { const mcpTools: ToolDefinition[] = [ { name: 'ghl_get_contacts', description: 'Get GHL contacts', inputSchema: { type: 'object', properties: {} } }, { name: 'ghl_send_message', description: 'Send GHL message', inputSchema: { type: 'object', properties: {} } } ]; vi.mocked(mockMcpClient.getTools).mockResolvedValue(mcpTools); const toolRouter = new ToolRouter(mockMcpClient); const tools = await toolRouter.getAllTools(); expect(appToolsRegistry.getDefinitions).toHaveBeenCalled(); expect(mockMcpClient.getTools).toHaveBeenCalled(); expect(tools).toHaveLength(4); // 2 app tools + 2 MCP tools expect(tools.map(t => t.name)).toContain('get_dashboard_stats'); expect(tools.map(t => t.name)).toContain('ghl_get_contacts'); }); it('should return only app tools when MCP client is null', async () => { const toolRouter = new ToolRouter(null); const tools = await toolRouter.getAllTools(); expect(appToolsRegistry.getDefinitions).toHaveBeenCalled(); expect(tools).toEqual(appToolDefinitions); expect(tools).toHaveLength(2); }); it('should return only app tools when MCP getTools fails', async () => { vi.mocked(mockMcpClient.getTools).mockRejectedValue(new Error('MCP connection failed')); const toolRouter = new ToolRouter(mockMcpClient); const tools = await toolRouter.getAllTools(); expect(tools).toEqual(appToolDefinitions); expect(tools).toHaveLength(2); }); it('should handle MCP returning empty tools array', async () => { vi.mocked(mockMcpClient.getTools).mockResolvedValue([]); const toolRouter = new ToolRouter(mockMcpClient); const tools = await toolRouter.getAllTools(); expect(tools).toEqual(appToolDefinitions); }); }); describe('execute', () => { it('should route app tool calls to appToolsRegistry', async () => { const toolCall: ToolCall = { id: 'tc-123', name: 'get_dashboard_stats', input: {} }; vi.mocked(appToolsRegistry.has).mockReturnValue(true); vi.mocked(appToolsRegistry.execute).mockResolvedValue({ toolCallId: '', success: true, result: { totalContacts: 100, totalConversations: 50 } }); const toolRouter = new ToolRouter(mockMcpClient); const result = await toolRouter.execute(toolCall, mockContext); expect(appToolsRegistry.has).toHaveBeenCalledWith('get_dashboard_stats'); expect(appToolsRegistry.execute).toHaveBeenCalledWith( 'get_dashboard_stats', {}, mockContext ); expect(result.toolCallId).toBe('tc-123'); expect(result.success).toBe(true); expect(result.result).toEqual({ totalContacts: 100, totalConversations: 50 }); }); it('should route MCP tool calls to MCPClient', async () => { const toolCall: ToolCall = { id: 'tc-456', name: 'ghl_get_contacts', input: { limit: 10 } }; vi.mocked(appToolsRegistry.has).mockReturnValue(false); vi.mocked(mockMcpClient.executeTool).mockResolvedValue({ toolCallId: '', success: true, result: [{ id: 'contact-1', name: 'John Doe' }] }); const toolRouter = new ToolRouter(mockMcpClient); const result = await toolRouter.execute(toolCall, mockContext); expect(appToolsRegistry.has).toHaveBeenCalledWith('ghl_get_contacts'); expect(mockMcpClient.executeTool).toHaveBeenCalledWith('ghl_get_contacts', { limit: 10 }); expect(result.toolCallId).toBe('tc-456'); expect(result.success).toBe(true); }); it('should return error when MCP client is not configured for MCP tools', async () => { const toolCall: ToolCall = { id: 'tc-789', name: 'ghl_get_contacts', input: {} }; vi.mocked(appToolsRegistry.has).mockReturnValue(false); const toolRouter = new ToolRouter(null); const result = await toolRouter.execute(toolCall, mockContext); expect(result.toolCallId).toBe('tc-789'); expect(result.success).toBe(false); expect(result.error).toBe('MCP client not configured'); }); it('should handle app tool execution errors', async () => { const toolCall: ToolCall = { id: 'tc-error', name: 'list_recent_contacts', input: {} }; vi.mocked(appToolsRegistry.has).mockReturnValue(true); vi.mocked(appToolsRegistry.execute).mockResolvedValue({ toolCallId: '', success: false, error: 'GHL not configured' }); const toolRouter = new ToolRouter(mockMcpClient); const result = await toolRouter.execute(toolCall, mockContext); expect(result.toolCallId).toBe('tc-error'); expect(result.success).toBe(false); expect(result.error).toBe('GHL not configured'); }); it('should handle MCP tool execution errors', async () => { const toolCall: ToolCall = { id: 'tc-mcp-error', name: 'ghl_send_message', input: { contactId: '123', message: 'Hello' } }; vi.mocked(appToolsRegistry.has).mockReturnValue(false); vi.mocked(mockMcpClient.executeTool).mockResolvedValue({ toolCallId: '', success: false, error: 'Tool execution failed: 500 - Internal server error' }); const toolRouter = new ToolRouter(mockMcpClient); const result = await toolRouter.execute(toolCall, mockContext); expect(result.toolCallId).toBe('tc-mcp-error'); expect(result.success).toBe(false); expect(result.error).toContain('Tool execution failed'); }); }); describe('executeAll', () => { it('should execute multiple tool calls in parallel', async () => { const toolCalls: ToolCall[] = [ { id: 'tc-1', name: 'get_dashboard_stats', input: {} }, { id: 'tc-2', name: 'ghl_get_contacts', input: { limit: 5 } } ]; vi.mocked(appToolsRegistry.has).mockImplementation((name) => name === 'get_dashboard_stats' ); vi.mocked(appToolsRegistry.execute).mockResolvedValue({ toolCallId: '', success: true, result: { totalContacts: 100 } }); vi.mocked(mockMcpClient.executeTool).mockResolvedValue({ toolCallId: '', success: true, result: [] }); const toolRouter = new ToolRouter(mockMcpClient); const results = await toolRouter.executeAll(toolCalls, mockContext); expect(results).toHaveLength(2); expect(results[0].toolCallId).toBe('tc-1'); expect(results[1].toolCallId).toBe('tc-2'); }); it('should handle partial failures in executeAll', async () => { const toolCalls: ToolCall[] = [ { id: 'tc-1', name: 'get_dashboard_stats', input: {} }, { id: 'tc-2', name: 'failing_tool', input: {} } ]; vi.mocked(appToolsRegistry.has).mockImplementation((name) => name === 'get_dashboard_stats' || name === 'failing_tool' ); vi.mocked(appToolsRegistry.execute) .mockResolvedValueOnce({ toolCallId: '', success: true, result: { totalContacts: 100 } }) .mockResolvedValueOnce({ toolCallId: '', success: false, error: 'Tool failed' }); const toolRouter = new ToolRouter(mockMcpClient); const results = await toolRouter.executeAll(toolCalls, mockContext); expect(results).toHaveLength(2); expect(results[0].success).toBe(true); expect(results[1].success).toBe(false); }); it('should return empty array for empty tool calls', async () => { const toolRouter = new ToolRouter(mockMcpClient); const results = await toolRouter.executeAll([], mockContext); expect(results).toEqual([]); }); }); describe('handles MCP not configured gracefully', () => { it('should work with null MCP client for app-only workflows', async () => { const toolCall: ToolCall = { id: 'tc-app', name: 'get_dashboard_stats', input: {} }; vi.mocked(appToolsRegistry.has).mockReturnValue(true); vi.mocked(appToolsRegistry.execute).mockResolvedValue({ toolCallId: '', success: true, result: { totalContacts: 50 } }); const toolRouter = new ToolRouter(null); // Get tools should work const tools = await toolRouter.getAllTools(); expect(tools).toEqual(appToolDefinitions); // Execute app tools should work const result = await toolRouter.execute(toolCall, mockContext); expect(result.success).toBe(true); }); it('should log error but return app tools when MCP fails', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.mocked(mockMcpClient.getTools).mockRejectedValue( new Error('Connection refused') ); const toolRouter = new ToolRouter(mockMcpClient); const tools = await toolRouter.getAllTools(); expect(consoleSpy).toHaveBeenCalledWith( 'Failed to get MCP tools:', expect.any(Error) ); expect(tools).toEqual(appToolDefinitions); consoleSpy.mockRestore(); }); }); });