Skip to main content
Glama

Nexus MCP Server

search.test.ts17.4 kB
import { describe, it, expect, beforeEach, vi, type MockedClass } from 'vitest'; import { SearchTool, createSearchTool, performSearch, } from '../../../src/tools/search'; import type { ChatCompletionResponse } from '../../../src/types/openrouter'; // Mock the OpenRouter client vi.mock('../../../src/clients/openrouter', () => { const mockClient = { chatCompletions: vi.fn(), testConnection: vi.fn(), getHeaders: vi.fn(), }; return { OpenRouterClient: vi.fn(() => mockClient), OpenRouterApiError: class extends Error { constructor( message: string, public statusCode: number, public type: string, public code: number ) { super(message); } }, AuthenticationError: class extends Error { constructor( message: string, public statusCode: number = 401, public code: number = 401 ) { super(message); } }, RateLimitError: class extends Error { constructor( message: string, public retryAfter?: number, public statusCode: number = 429, public code: number = 429 ) { super(message); this.retryAfter = retryAfter; } }, ServerError: class extends Error { constructor( message: string, public statusCode: number, public code: number ) { super(message); } }, }; }); // Mock winston to avoid console output during tests vi.mock('winston', () => ({ default: { createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }), format: { combine: vi.fn(() => ({})), timestamp: vi.fn(), errors: vi.fn(), json: vi.fn(), colorize: vi.fn(), simple: vi.fn(), }, transports: { Console: vi.fn(), }, }, })); describe('SearchTool', () => { const mockApiKey = 'sk-or-test-api-key-12345678901234'; let searchTool: SearchTool; let mockClient: { chatCompletions: ReturnType<typeof vi.fn>; testConnection: ReturnType<typeof vi.fn>; getHeaders: ReturnType<typeof vi.fn>; }; beforeEach(async () => { vi.clearAllMocks(); // Set environment variable for configuration process.env.OPENROUTER_API_KEY = mockApiKey; // Reset ConfigurationManager singleton const { ConfigurationManager } = await import( '../../../src/config/manager' ); ConfigurationManager['instance'] = null; searchTool = new SearchTool(mockApiKey); // Get the mock client instance const openRouterModule = await import('../../../src/clients/openrouter'); const MockClient = openRouterModule.OpenRouterClient as unknown as MockedClass< typeof openRouterModule.OpenRouterClient >; mockClient = MockClient.mock.results[MockClient.mock.results.length - 1].value; }); describe('constructor', () => { it('should create SearchTool with API key', () => { expect(searchTool).toBeInstanceOf(SearchTool); }); it('should initialize OpenRouter client with correct config', async () => { const openRouterModule = await import('../../../src/clients/openrouter'); const MockClient = openRouterModule.OpenRouterClient as unknown as MockedClass< typeof openRouterModule.OpenRouterClient >; expect(MockClient).toHaveBeenCalledWith({ apiKey: mockApiKey, userAgent: 'nexus-mcp/1.0.0', timeout: 30000, maxRetries: 3, }); }); }); describe('search', () => { const mockApiResponse: ChatCompletionResponse = { id: 'test-123', object: 'chat.completion', created: 1640995200, model: 'perplexity/sonar', choices: [ { index: 0, message: { role: 'assistant', content: 'This is a test response with source https://example.com', }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 10, completion_tokens: 20, total_tokens: 30, }, }; it('should perform successful search with valid input', async () => { mockClient.chatCompletions.mockResolvedValue(mockApiResponse); const input = { query: 'test query', model: 'perplexity/sonar' as const, maxTokens: 1000, temperature: 0.3, }; const result = await searchTool.search(input); expect(result.success).toBe(true); expect(result.result?.content).toBe( 'This is a test response with source https://example.com' ); expect(result.result?.metadata.query).toBe('test query'); expect(result.result?.metadata.model).toBe('perplexity/sonar'); expect(result.requestId).toBe('test-123'); }); it('should apply defaults for optional parameters', async () => { mockClient.chatCompletions.mockResolvedValue(mockApiResponse); const input = { query: 'test query' }; await searchTool.search(input); expect(mockClient.chatCompletions).toHaveBeenCalledWith({ model: 'perplexity/sonar', messages: [{ role: 'user', content: 'test query' }], temperature: 0.3, max_tokens: 1000, top_p: 1.0, frequency_penalty: 0.0, presence_penalty: 0.0, stop: undefined, stream: false, }); }); it('should handle validation errors', async () => { const input = { query: '' }; // Invalid empty query const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('validation'); expect(result.error).toContain('must be at least 1 character'); }); it('should handle authentication errors', async () => { const openRouterModule = await import('../../../src/clients/openrouter'); mockClient.chatCompletions.mockRejectedValue( new openRouterModule.AuthenticationError('Invalid API key') ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('auth'); expect(result.error).toBe('Authentication failed: Invalid API key'); }); it('should handle rate limit errors', async () => { const openRouterModule = await import('../../../src/clients/openrouter'); mockClient.chatCompletions.mockRejectedValue( new openRouterModule.RateLimitError('Rate limited', 60) ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('rate_limit'); expect(result.error).toBe('Rate limit exceeded (retry after 60s)'); }); it('should handle server errors', async () => { const openRouterModule = await import('../../../src/clients/openrouter'); mockClient.chatCompletions.mockRejectedValue( new openRouterModule.ServerError('Internal server error', 500, 500) ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('api'); expect(result.error).toBe('OpenRouter service temporarily unavailable'); }); it('should handle timeout errors', async () => { mockClient.chatCompletions.mockRejectedValue( new Error('Request timeout after 30000ms') ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('timeout'); expect(result.error).toBe('Request timed out - please try again'); }); it('should handle network errors', async () => { mockClient.chatCompletions.mockRejectedValue( new Error('Network error: Connection failed') ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('network'); expect(result.error).toBe('Network error - please check your connection'); }); it('should handle unknown errors', async () => { mockClient.chatCompletions.mockRejectedValue(new Error('Unknown error')); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('unknown'); expect(result.error).toBe('Unknown error'); }); it('should handle invalid input format', async () => { const input = 'not an object'; const result = await searchTool.search(input); expect(result.success).toBe(false); expect(result.errorType).toBe('validation'); }); it('should measure response time', async () => { mockClient.chatCompletions.mockImplementation( () => new Promise(resolve => setTimeout(() => resolve(mockApiResponse), 100) ) ); const input = { query: 'test query' }; const result = await searchTool.search(input); expect(result.success).toBe(true); expect(result.result?.metadata.responseTime).toBeGreaterThan(90); }); it('should deduplicate concurrent identical requests', async () => { mockClient.chatCompletions.mockImplementation( () => new Promise(resolve => setTimeout(() => resolve(mockApiResponse), 50)) ); const input = { query: 'concurrent test query' }; // Start multiple identical requests concurrently const promises = [ searchTool.search(input), searchTool.search(input), searchTool.search(input), ]; const results = await Promise.all(promises); // All should succeed and have the same result expect(results[0].success).toBe(true); expect(results[1].success).toBe(true); expect(results[2].success).toBe(true); expect(results[0].requestId).toBe(results[1].requestId); expect(results[0].requestId).toBe(results[2].requestId); // But the API should only be called once due to deduplication expect(mockClient.chatCompletions).toHaveBeenCalledTimes(1); // Check deduplication stats const stats = searchTool.getDeduplicationStats(); expect(stats.uniqueRequests).toBe(1); expect(stats.deduplicatedRequests).toBe(2); expect(stats.deduplicationRatio).toBe(2 / 3); }); }); describe('testConnection', () => { it('should return true for successful connection', async () => { mockClient.testConnection.mockResolvedValue(true); const result = await searchTool.testConnection(); expect(result).toBe(true); }); it('should return false for failed connection', async () => { mockClient.testConnection.mockResolvedValue(false); const result = await searchTool.testConnection(); expect(result).toBe(false); }); it('should return false for connection error', async () => { mockClient.testConnection.mockRejectedValue( new Error('Connection failed') ); const result = await searchTool.testConnection(); expect(result).toBe(false); }); }); describe('getClientInfo', () => { it('should return client configuration without API key', () => { mockClient.getHeaders.mockReturnValue({ Authorization: 'Bearer sk-or-test', 'Content-Type': 'application/json', 'User-Agent': 'test-agent', }); const info = searchTool.getClientInfo(); expect(info.headers['Authorization']).toBeUndefined(); expect(info.headers['Content-Type']).toBe('application/json'); expect(info.headers['User-Agent']).toBe('test-agent'); expect(info.baseUrl).toBe('https://openrouter.ai/api/v1'); }); }); describe('getDeduplicationStats', () => { it('should return deduplication statistics', () => { const stats = searchTool.getDeduplicationStats(); expect(stats).toHaveProperty('pendingRequests'); expect(stats).toHaveProperty('deduplicatedRequests'); expect(stats).toHaveProperty('uniqueRequests'); expect(stats).toHaveProperty('maxConcurrentRequests'); expect(stats).toHaveProperty('deduplicationRatio'); expect(typeof stats.pendingRequests).toBe('number'); expect(typeof stats.deduplicatedRequests).toBe('number'); expect(typeof stats.uniqueRequests).toBe('number'); expect(typeof stats.maxConcurrentRequests).toBe('number'); expect(typeof stats.deduplicationRatio).toBe('number'); }); }); describe('getCacheStats', () => { it('should return cache statistics', () => { const stats = searchTool.getCacheStats(); expect(stats).toHaveProperty('hits'); expect(stats).toHaveProperty('misses'); expect(stats).toHaveProperty('size'); expect(stats).toHaveProperty('maxSize'); expect(stats).toHaveProperty('hitRatio'); expect(typeof stats.hits).toBe('number'); expect(typeof stats.misses).toBe('number'); expect(typeof stats.size).toBe('number'); expect(typeof stats.maxSize).toBe('number'); expect(typeof stats.hitRatio).toBe('number'); }); }); describe('getPerformanceMetrics', () => { const testApiResponse = { id: 'metrics-test-123', object: 'chat.completion' as const, created: 1640995200, model: 'perplexity/sonar', choices: [ { index: 0, message: { role: 'assistant' as const, content: 'Test response for metrics with https://example.com', }, finish_reason: 'stop' as const, }, ], usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25, }, }; it('should return performance metrics', () => { const metrics = searchTool.getPerformanceMetrics(); expect(metrics).toHaveProperty('averageProcessingTime'); expect(metrics).toHaveProperty('averageSourceCount'); expect(metrics).toHaveProperty('averageContentLength'); expect(metrics).toHaveProperty('averageMemoryUsage'); expect(metrics).toHaveProperty('totalRequests'); expect(metrics).toHaveProperty('slowestRequest'); expect(metrics).toHaveProperty('fastestRequest'); expect(typeof metrics.averageProcessingTime).toBe('number'); expect(typeof metrics.averageSourceCount).toBe('number'); expect(typeof metrics.averageContentLength).toBe('number'); expect(typeof metrics.averageMemoryUsage).toBe('number'); expect(typeof metrics.totalRequests).toBe('number'); }); it('should track metrics after search operations', async () => { mockClient.chatCompletions.mockResolvedValue(testApiResponse); const input = { query: 'test query for metrics' }; await searchTool.search(input); const metrics = searchTool.getPerformanceMetrics(); expect(metrics.totalRequests).toBe(1); expect(metrics.averageProcessingTime).toBeGreaterThan(0); expect(metrics.averageContentLength).toBeGreaterThan(0); expect(metrics.slowestRequest).toBeDefined(); expect(metrics.fastestRequest).toBeDefined(); }); it('should clear performance metrics', async () => { mockClient.chatCompletions.mockResolvedValue(testApiResponse); await searchTool.search({ query: 'test query' }); let metrics = searchTool.getPerformanceMetrics(); expect(metrics.totalRequests).toBe(1); searchTool.clearPerformanceMetrics(); metrics = searchTool.getPerformanceMetrics(); expect(metrics.totalRequests).toBe(0); expect(metrics.averageProcessingTime).toBe(0); }); }); }); describe('Factory Functions', () => { describe('createSearchTool', () => { it('should create SearchTool instance', () => { const searchTool = createSearchTool('test-api-key'); expect(searchTool).toBeInstanceOf(SearchTool); }); }); describe('performSearch', () => { it('should perform search with factory function', async () => { const openRouterModule = await import('../../../src/clients/openrouter'); const MockClient = openRouterModule.OpenRouterClient as unknown as MockedClass< typeof openRouterModule.OpenRouterClient >; const mockClient = MockClient.mock.results[MockClient.mock.results.length - 1].value; const mockResponse: ChatCompletionResponse = { id: 'test-123', object: 'chat.completion', created: 1640995200, model: 'perplexity/sonar', choices: [ { index: 0, message: { role: 'assistant', content: 'Test response', }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15, }, }; mockClient.chatCompletions.mockResolvedValue(mockResponse); const result = await performSearch('test query', 'test-api-key', { temperature: 0.5, }); expect(result.success).toBe(true); expect(result.result?.content).toBe('Test response'); expect(result.result?.metadata.temperature).toBe(0.5); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adawalli/nexus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server