/** * Unit tests for MCP Manager module * * Tests web search MCP configuration logic without filesystem dependencies. * These tests focus on the pure logic functions that can be tested without * modifying the actual config files. */ import { describe, it, expect } from 'bun:test'; /** * Test helper: Simulate hasWebSearch detection logic * This matches the logic in mcp-manager.ts hasMcpWebSearch() */ function detectWebSearchMcp(mcpServers: Record): boolean { return Object.keys(mcpServers).some((key) => { const lowerKey = key.toLowerCase(); return ( lowerKey.includes('web-search') || lowerKey.includes('websearch') || lowerKey.includes('tavily') || lowerKey.includes('brave') ); }); } /** * Test helper: Simulate provider configuration logic * Returns which providers would be added given config and env */ function getProvidersToAdd( wsConfig: { enabled: boolean; provider: string; fallback: boolean }, hasExistingWebSearch: boolean, apiKeys: { brave?: string; tavily?: string } ): string[] { if (!wsConfig.enabled) return []; if (hasExistingWebSearch) return []; const providers: string[] = []; if (wsConfig.provider === 'auto') { providers.push('web-search-prime'); if (wsConfig.fallback) { if (apiKeys.brave) providers.push('brave-search'); if (apiKeys.tavily) providers.push('tavily'); } } else if (wsConfig.provider === 'web-search-prime') { providers.push('web-search-prime'); } else if (wsConfig.provider === 'brave') { if (apiKeys.brave) { providers.push('brave-search'); } else if (wsConfig.fallback) { // Fallback chain providers.push('web-search-prime'); if (apiKeys.tavily) providers.push('tavily'); } } else if (wsConfig.provider === 'tavily') { if (apiKeys.tavily) { providers.push('tavily'); } else if (wsConfig.fallback) { // Fallback chain providers.push('web-search-prime'); if (apiKeys.brave) providers.push('brave-search'); } } return providers; } describe('mcp-manager logic', () => { describe('web search detection', () => { it('should detect web-search-prime', () => { const servers = { 'web-search-prime': { type: 'http' } }; expect(detectWebSearchMcp(servers)).toBe(true); }); it('should detect brave-search', () => { const servers = { 'brave-search': { type: 'stdio' } }; expect(detectWebSearchMcp(servers)).toBe(true); }); it('should detect tavily', () => { const servers = { tavily: { type: 'stdio' } }; expect(detectWebSearchMcp(servers)).toBe(true); }); it('should be case-insensitive', () => { expect(detectWebSearchMcp({ 'WebSearch-Custom': {} })).toBe(true); expect(detectWebSearchMcp({ 'WEB-SEARCH': {} })).toBe(true); expect(detectWebSearchMcp({ TAVILY: {} })).toBe(true); expect(detectWebSearchMcp({ 'Brave-Search': {} })).toBe(true); }); it('should not detect unrelated MCPs', () => { expect(detectWebSearchMcp({ 'my-custom-mcp': {} })).toBe(false); expect(detectWebSearchMcp({ filesystem: {} })).toBe(false); expect(detectWebSearchMcp({ 'github-copilot': {} })).toBe(false); }); it('should return false for empty servers', () => { expect(detectWebSearchMcp({})).toBe(false); }); }); describe('provider selection logic', () => { const defaultConfig = { enabled: true, provider: 'auto', fallback: true }; const noApiKeys = {}; const braveOnly = { brave: 'test-key' }; const tavilyOnly = { tavily: 'test-key' }; const bothKeys = { brave: 'brave-key', tavily: 'tavily-key' }; it('should add web-search-prime in auto mode', () => { const providers = getProvidersToAdd(defaultConfig, false, noApiKeys); expect(providers).toContain('web-search-prime'); }); it('should add brave-search when API key available in auto mode', () => { const providers = getProvidersToAdd(defaultConfig, false, braveOnly); expect(providers).toContain('web-search-prime'); expect(providers).toContain('brave-search'); }); it('should add tavily when API key available in auto mode', () => { const providers = getProvidersToAdd(defaultConfig, false, tavilyOnly); expect(providers).toContain('web-search-prime'); expect(providers).toContain('tavily'); }); it('should add all providers when both API keys available', () => { const providers = getProvidersToAdd(defaultConfig, false, bothKeys); expect(providers).toContain('web-search-prime'); expect(providers).toContain('brave-search'); expect(providers).toContain('tavily'); }); it('should not add fallbacks when fallback=false', () => { const config = { enabled: true, provider: 'auto', fallback: false }; const providers = getProvidersToAdd(config, false, bothKeys); expect(providers).toEqual(['web-search-prime']); }); it('should skip when disabled', () => { const config = { enabled: false, provider: 'auto', fallback: true }; const providers = getProvidersToAdd(config, false, bothKeys); expect(providers).toEqual([]); }); it('should skip when web search already exists', () => { const providers = getProvidersToAdd(defaultConfig, true, bothKeys); expect(providers).toEqual([]); }); it('should use specific provider when configured', () => { const config = { enabled: true, provider: 'brave', fallback: false }; const providers = getProvidersToAdd(config, false, braveOnly); expect(providers).toEqual(['brave-search']); }); it('should fallback when specific provider not available', () => { const config = { enabled: true, provider: 'tavily', fallback: true }; // No tavily key, should fallback const providers = getProvidersToAdd(config, false, braveOnly); expect(providers).toContain('web-search-prime'); expect(providers).toContain('brave-search'); expect(providers).not.toContain('tavily'); }); it('should return empty when provider unavailable and fallback=false', () => { const config = { enabled: true, provider: 'brave', fallback: false }; // No brave key and no fallback const providers = getProvidersToAdd(config, false, noApiKeys); expect(providers).toEqual([]); }); }); describe('MCP server config structures', () => { it('should define correct web-search-prime structure', () => { const webSearchPrimeConfig = { type: 'http', url: 'https://api.z.ai/api/mcp/web_search_prime/mcp', headers: {}, }; expect(webSearchPrimeConfig.type).toBe('http'); expect(webSearchPrimeConfig.url).toContain('web_search_prime'); }); it('should define correct brave-search structure', () => { const braveConfig = { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-brave-search'], env: { BRAVE_API_KEY: 'test-key' }, }; expect(braveConfig.type).toBe('stdio'); expect(braveConfig.command).toBe('npx'); expect(braveConfig.args).toContain('@modelcontextprotocol/server-brave-search'); expect(braveConfig.env.BRAVE_API_KEY).toBe('test-key'); }); it('should define correct tavily structure', () => { const tavilyConfig = { type: 'stdio', command: 'npx', args: ['-y', '@tavily/mcp-server'], env: { TAVILY_API_KEY: 'test-key' }, }; expect(tavilyConfig.type).toBe('stdio'); expect(tavilyConfig.args).toContain('@tavily/mcp-server'); expect(tavilyConfig.env.TAVILY_API_KEY).toBe('test-key'); }); }); describe('hook configuration', () => { it('should define correct PreToolUse hook structure', () => { const hookConfig = { PreToolUse: [ { matcher: 'WebSearch', hooks: [ { type: 'command', command: 'node "/path/to/hook.cjs"', timeout: 5, }, ], }, ], }; expect(hookConfig.PreToolUse).toBeDefined(); expect(hookConfig.PreToolUse[0].matcher).toBe('WebSearch'); expect(hookConfig.PreToolUse[0].hooks[0].type).toBe('command'); expect(hookConfig.PreToolUse[0].hooks[0].timeout).toBe(5); }); }); describe('MCP config file path', () => { it('should be located in .claude directory', () => { // The MCP config path should follow this pattern const expectedPathPattern = '.claude/.mcp.json'; const testPath = '/home/user/.claude/.mcp.json'; expect(testPath).toContain(expectedPathPattern); }); }); describe('WebSearch config defaults', () => { it('should have correct default values', () => { const defaults = { enabled: true, provider: 'auto', fallback: true, }; expect(defaults.enabled).toBe(true); expect(defaults.provider).toBe('auto'); expect(defaults.fallback).toBe(true); }); it('should validate provider options', () => { const validProviders = ['auto', 'web-search-prime', 'brave', 'tavily']; expect(validProviders).toContain('auto'); expect(validProviders).toContain('web-search-prime'); expect(validProviders).toContain('brave'); expect(validProviders).toContain('tavily'); }); }); });