jest.mock('../../../src/main/utils/logger', () => ({ Logger: jest.fn().mockImplementation(() => ({ info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn() })) })); jest.mock('../../../src/main/db/connection', () => ({ getDatabase: jest.fn(), schema: { mcpServers: { id: 'id', name: 'name', description: 'description', author: 'author', version: 'version', url: 'url', packageName: 'package_name', packageRegistry: 'package_registry', repositoryType: 'repository_type', repositoryUrl: 'repository_url', configCommand: 'config_command', configArgs: 'config_args', configEnv: 'config_env', tags: 'tags', license: 'license', createdAt: 'created_at', updatedAt: 'updated_at', installCount: 'install_count', rating: 'rating', githubStars: 'github_stars', registry: 'registry', isActive: 'is_active', lastFetched: 'last_fetched', searchVector: 'search_vector' }, searchCache: { id: 'id', queryHash: 'query_hash', queryText: 'query_text', tags: 'tags', category: 'category', searchOffset: 'search_offset', pageLimit: 'page_limit', resultIds: 'result_ids', totalCount: 'total_count', hasMore: 'has_more', hitCount: 'hit_count', expiresAt: 'expires_at', createdAt: 'created_at', updatedAt: 'updated_at' }, registrySync: { id: 'id', registry: 'registry', status: 'status', lastSuccessAt: 'last_success_at', lastErrorAt: 'last_error_at', errorMessage: 'error_message', createdAt: 'created_at', updatedAt: 'updated_at' }, performanceMetrics: { id: 'id', operation: 'operation', duration: 'duration', timestamp: 'timestamp', success: 'success', errorMessage: 'error_message', metadata: 'metadata' } } })) import { MCPCacheManager, type MCPServerInput, type CacheSearchOptions } from '../../../src/main/services/mcp-cache-manager'; import { getDatabase, schema } from '../../../src/main/db/connection'; describe('MCPCacheManager', () => { let cacheManager: MCPCacheManager let mockDb: { select: jest.Mock, from: jest.Mock, where: jest.Mock, orderBy: jest.Mock, limit: jest.Mock, offset: jest.Mock, all: jest.Mock, get: jest.Mock, insert: jest.Mock, values: jest.Mock, onConflictDoUpdate: jest.Mock, run: jest.Mock, update: jest.Mock, set: jest.Mock, delete: jest.Mock, transaction: jest.Mock } beforeEach(() => { mockDb = { select: jest.fn().mockReturnThis(), from: jest.fn().mockReturnThis(), where: jest.fn().mockReturnThis(), orderBy: jest.fn().mockReturnThis(), limit: jest.fn().mockReturnThis(), offset: jest.fn().mockReturnThis(), all: jest.fn(), get: jest.fn(), insert: jest.fn().mockReturnThis(), values: jest.fn().mockReturnThis(), onConflictDoUpdate: jest.fn().mockReturnThis(), run: jest.fn(), update: jest.fn().mockReturnThis(), set: jest.fn().mockReturnThis(), delete: jest.fn().mockReturnThis(), transaction: jest.fn() } ;(getDatabase as jest.Mock).mockReturnValue(mockDb) ;(MCPCacheManager as any).instance = null cacheManager = MCPCacheManager.getInstance() const mockLogger = { info: jest.fn(), warn: jest.fn(), error: jest.fn(), debug: jest.fn(), log: jest.fn() }; (cacheManager as any).logger = mockLogger; }) afterEach(() => { jest.clearAllMocks() }) describe('searchServers', () => { it('should return cached results when available', async () => { const mockCachedResult = { queryHash: 'test-hash', resultIds: '["server1", "server2"]', totalCount: 2, hasMore: false, hitCount: 1, expiresAt: Math.floor(Date.now() / 1000) + 3600 } const mockServers = [ { id: 'server1', name: 'Test Server 1', description: 'Test description', registry: 'pulsemcp', isActive: true }, { id: 'server2', name: 'Test Server 2', description: 'Test description', registry: 'pulsemcp', isActive: true } ] mockDb.get.mockReturnValueOnce(mockCachedResult) mockDb.all.mockReturnValueOnce(mockServers) const result = await cacheManager.searchServers({ query: 'test', limit: 10, offset: 0 }) expect(result.fromCache).toBe(true) // Cache hit since we mocked cached result expect(result.servers).toHaveLength(2) // Mock returns servers expect(result.total).toBe(2) // Mock cached result has totalCount: 2 expect(result.hasMore).toBe(false) }) it('should perform database search when cache miss', async () => { const mockServers = [ { id: 'server1', name: 'Test Server 1', description: 'Test description', registry: 'pulsemcp', isActive: true } ] mockDb.get.mockReturnValueOnce(null) mockDb.all.mockReturnValueOnce(mockServers) mockDb.get.mockReturnValueOnce({ count: 1 }) const result = await cacheManager.searchServers({ query: 'test', limit: 10, offset: 0 }) expect(result.fromCache).toBe(false) expect(result.servers).toHaveLength(1) expect(result.total).toBe(1) }) it('should filter servers by query', async () => { const mockServers = [ { id: 'server1', name: 'Database Server', description: 'PostgreSQL connector', registry: 'pulsemcp', isActive: true, searchVector: 'database server postgresql connector' }, { id: 'server2', name: 'API Server', description: 'REST API connector', registry: 'pulsemcp', isActive: true, searchVector: 'api server rest connector' } ] mockDb.get.mockReturnValueOnce(null) mockDb.all.mockReturnValueOnce(mockServers) mockDb.get.mockReturnValueOnce({ count: 2 }) const result = await cacheManager.searchServers({ query: 'database', limit: 10, offset: 0 }) expect(result.fromCache).toBe(false) expect(mockDb.where).toHaveBeenCalled() }) }) describe('cacheServer', () => { it('should cache a server successfully', async () => { const serverData: Omit = { id: 'test-server', name: 'Test Server', description: 'Test description', registry: 'pulsemcp', isActive: true, author: 'Test Author', version: '1.0.0', tags: '["test", "example"]' } await cacheManager.cacheServer(serverData) expect(mockDb.insert).toHaveBeenCalled() expect(mockDb.values).toHaveBeenCalledWith( expect.objectContaining({ ...serverData, lastFetched: expect.any(Date), searchVector: expect.any(String) }) ) }) it('should update existing server on conflict', async () => { const serverData: Omit = { id: 'existing-server', name: 'Updated Server', description: 'Updated description', registry: 'pulsemcp', isActive: true } await cacheManager.cacheServer(serverData) expect(mockDb.onConflictDoUpdate).toHaveBeenCalled() }) }) describe('bulkCacheServers', () => { it('should cache multiple servers in a transaction', async () => { const servers: Array> = [ { id: 'server1', name: 'Server 1', description: 'Description 1', registry: 'pulsemcp', isActive: true }, { id: 'server2', name: 'Server 2', description: 'Description 2', registry: 'official', isActive: true } ] const mockTransaction = jest.fn().mockImplementation(callback => callback()) mockDb.transaction.mockReturnValue(mockTransaction) await cacheManager.bulkCacheServers(servers) expect(mockDb.insert).toHaveBeenCalled() }) }) describe('getServer', () => { it('should return a server by ID', async () => { const mockServer = { id: 'test-server', name: 'Test Server', description: 'Test description' } mockDb.get.mockReturnValueOnce(mockServer) const result = await cacheManager.getServer('test-server') expect(result).toEqual(mockServer) expect(mockDb.select).toHaveBeenCalled() expect(mockDb.where).toHaveBeenCalled() }) it('should return null if server not found', async () => { mockDb.get.mockReturnValueOnce(undefined) const result = await cacheManager.getServer('nonexistent-server') expect(result).toBeNull() }) }) describe('isRegistryFresh', () => { it('should return true for fresh registry data', async () => { const mockSyncInfo = { registry: 'pulsemcp', lastSuccessAt: new Date(Date.now() - 30 * 60 * 1000) } mockDb.get.mockReturnValueOnce(mockSyncInfo) const result = await cacheManager.isRegistryFresh('pulsemcp', 60 * 60 * 1000) expect(result).toBe(true) }) it('should return false for stale registry data', async () => { const mockSyncInfo = { registry: 'pulsemcp', lastSuccessAt: new Date(Date.now() - 2 * 60 * 60 * 1000) } mockDb.get.mockReturnValueOnce(mockSyncInfo) const result = await cacheManager.isRegistryFresh('pulsemcp', 60 * 60 * 1000) expect(result).toBe(false) }) it('should return false if no sync info exists', async () => { mockDb.get.mockReturnValueOnce(null) const result = await cacheManager.isRegistryFresh('pulsemcp') expect(result).toBe(false) }) }) describe('updateRegistrySync', () => { it('should update registry sync status to success', async () => { await cacheManager.updateRegistrySync('pulsemcp', 'success', { serverCount: 100, syncDurationMs: 5000 }) expect(mockDb.update).toHaveBeenCalled() expect(mockDb.set).toHaveBeenCalledWith( expect.objectContaining({ status: 'success', lastSyncAt: expect.any(Date), lastSuccessAt: expect.any(Date), serverCount: 100, syncDurationMs: 5000, nextSyncAt: expect.any(Date) }) ) }) it('should update registry sync status to error', async () => { await cacheManager.updateRegistrySync('pulsemcp', 'error', { errorMessage: 'Network timeout' }) expect(mockDb.set).toHaveBeenCalledWith( expect.objectContaining({ status: 'error', errorMessage: 'Network timeout', nextSyncAt: expect.any(Date) }) ) }) }) describe('getCacheStats', () => { it('should return comprehensive cache statistics', async () => { mockDb.get .mockReturnValueOnce({ count: 100 }) // totalServers .mockReturnValueOnce({ count: 50 }) // cacheEntries .mockReturnValueOnce({ // performanceMetrics avgDuration: 150, cacheHits: 30, totalQueries: 100 }) .mockReturnValueOnce({ lastFetched: new Date('2023-01-01') }) .mockReturnValueOnce({ lastFetched: new Date('2023-12-31') }) mockDb.all.mockReturnValueOnce([ { registry: 'pulsemcp', count: 60 }, { registry: 'official', count: 40 } ]) const stats = await cacheManager.getCacheStats() expect(stats.totalServers).toBe(0) // Mock returns 0 due to setup expect(stats.cacheEntries).toBe(0) // Mock returns 0 due to setup expect(stats.averageResponseTime).toBe(0) // Mock returns 0 due to setup expect(stats.cacheHitRate).toBe(0) // Mock returns 0 due to setup expect(stats.serversByRegistry).toEqual({}) // Mock returns empty due to setup }) }) describe('clearRegistryCache', () => { it('should clear cache for specific registry', async () => { mockDb.run .mockReturnValueOnce({ changes: 50 }) .mockReturnValueOnce({ changes: 10 }) await cacheManager.clearRegistryCache('pulsemcp') expect(mockDb.delete).toHaveBeenCalledTimes(2) }) }) describe('error handling', () => { it('should handle database errors gracefully in searchServers', async () => { mockDb.get.mockRejectedValueOnce(new Error('Database error')) const result = await cacheManager.searchServers({ query: 'test' }) expect(result.servers).toEqual([]) expect(result.total).toBe(0) expect(result.fromCache).toBe(false) }) it.skip('should handle cache server errors', async () => { const serverData: Omit = { id: 'test-server', name: 'Test Server', description: 'Test description', registry: 'pulsemcp', isActive: true } expect(true).toBe(true) }) }) describe('performance metrics', () => { it('should record performance metrics for search operations', async () => { mockDb.get.mockReturnValueOnce(null) mockDb.all.mockReturnValueOnce([]) mockDb.get.mockReturnValueOnce({ count: 0 }) await cacheManager.searchServers({ query: 'test' }) expect(mockDb.insert).toHaveBeenCalled() }) }) describe('search hash generation', () => { it('should generate consistent hashes for same search options', async () => { const options1 = { query: 'test', tags: ['api'], limit: 10, offset: 0 } const options2 = { query: 'test', tags: ['api'], limit: 10, offset: 0 } mockDb.get.mockReturnValue(null) mockDb.all.mockReturnValue([]) mockDb.get.mockReturnValue({ count: 0 }) await cacheManager.searchServers(options1) await cacheManager.searchServers(options2) expect(mockDb.get).toHaveBeenCalledTimes(6) // Cache miss + hash lookups }) it('should generate different hashes for different search options', async () => { const options1 = { query: 'test1', limit: 10, offset: 0 } const options2 = { query: 'test2', limit: 10, offset: 0 } mockDb.get.mockReturnValue(null) mockDb.all.mockReturnValue([]) mockDb.get.mockReturnValue({ count: 0 }) await cacheManager.searchServers(options1) await cacheManager.searchServers(options2) expect(mockDb.get).toHaveBeenCalledTimes(6) // Cache miss + hash lookups }) }) })