cre-sync/__tests__/lib/control-center/tool-router.test.ts
BusyBee3333 4e6467ffb0 Add CRESync CRM application with Setup page
- Build complete Next.js CRM for commercial real estate
- Add authentication with JWT sessions and role-based access
- Add GoHighLevel API integration for contacts, conversations, opportunities
- Add AI-powered Control Center with tool calling
- Add Setup page with onboarding checklist (/setup)
- Add sidebar navigation with Setup menu item
- Fix type errors in onboarding API, GHL services, and control center tools
- Add Prisma schema with SQLite for local development
- Add UI components with clay morphism design system

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:30:55 -05:00

342 lines
11 KiB
TypeScript

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();
});
});
});