/** * Vector Memory Tests - Phase 4.2 Semantic Search * * Tests for vector-based semantic memory search: * - VectorMemoryService initialization * - Semantic search with OpenViking * - Similar memory finding * - Clustering functionality */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { VectorMemoryService, getVectorMemory, resetVectorMemory, semanticSearch, findSimilarMemories, isVectorSearchAvailable, DEFAULT_VECTOR_CONFIG, type VectorSearchOptions, type VectorSearchResult, } from '../../desktop/src/lib/vector-memory'; import { getVikingClient, resetVikingClient } from '../../desktop/src/lib/viking-client'; import { getMemoryManager, resetMemoryManager } from '../../desktop/src/lib/agent-memory'; // === Mock Dependencies === const mockVikingClient = { isAvailable: vi.fn(async () => true), find: vi.fn(async () => [ { uri: 'memories/agent1/memory1', content: '用户偏好简洁的回答', score: 0.9, metadata: { tags: ['preference'] } }, { uri: 'memories/agent1/memory2', content: '项目使用 TypeScript', score: 0.7, metadata: { tags: ['fact'] } }, { uri: 'memories/agent1/memory3', content: '需要完成性能测试', score: 0.5, metadata: { tags: ['task'] } }, ]), addResource: vi.fn(async () => ({ uri: 'test', status: 'ok' })), removeResource: vi.fn(async () => undefined), }; vi.mock('../../desktop/src/lib/viking-client', () => ({ getVikingClient: vi.fn(() => mockVikingClient), resetVikingClient: vi.fn(), VikingHttpClient: vi.fn(), })); const mockMemoryManager = { getByAgent: vi.fn(() => [ { id: 'memory1', agentId: 'agent1', content: '用户偏好简洁的回答', type: 'preference', importance: 7, createdAt: new Date().toISOString(), source: 'auto', tags: ['style'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, { id: 'memory2', agentId: 'agent1', content: '项目使用 TypeScript', type: 'fact', importance: 6, createdAt: new Date().toISOString(), source: 'auto', tags: ['tech'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, { id: 'memory3', agentId: 'agent1', content: '需要完成性能测试', type: 'task', importance: 8, createdAt: new Date().toISOString(), source: 'auto', tags: ['todo'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, ]), getAll: vi.fn(async () => [ { id: 'memory1', agentId: 'agent1', content: '用户偏好简洁的回答', type: 'preference', importance: 7, createdAt: new Date().toISOString(), source: 'auto', tags: ['style'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, { id: 'memory2', agentId: 'agent1', content: '项目使用 TypeScript', type: 'fact', importance: 6, createdAt: new Date().toISOString(), source: 'auto', tags: ['tech'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, { id: 'memory3', agentId: 'agent1', content: '需要完成性能测试', type: 'task', importance: 8, createdAt: new Date().toISOString(), source: 'auto', tags: ['todo'], lastAccessedAt: new Date().toISOString(), accessCount: 0 }, ]), save: vi.fn(async () => 'memory-id'), }; vi.mock('../../desktop/src/lib/agent-memory', () => ({ getMemoryManager: vi.fn(() => mockMemoryManager), resetMemoryManager: vi.fn(), })); // === VectorMemoryService Tests === describe('VectorMemoryService', () => { let service: VectorMemoryService; beforeEach(() => { vi.clearAllMocks(); resetVectorMemory(); resetVikingClient(); service = new VectorMemoryService(); }); afterEach(() => { resetVectorMemory(); }); describe('Initialization', () => { it('should initialize with default config', () => { const config = service.getConfig(); expect(config.enabled).toBe(true); expect(config.defaultTopK).toBe(10); expect(config.defaultMinScore).toBe(0.3); expect(config.defaultLevel).toBe('L1'); }); it('should accept custom config', () => { const customService = new VectorMemoryService({ defaultTopK: 20, defaultMinScore: 0.5, }); const config = customService.getConfig(); expect(config.defaultTopK).toBe(20); expect(config.defaultMinScore).toBe(0.5); }); it('should update config', () => { service.updateConfig({ defaultTopK: 15 }); const config = service.getConfig(); expect(config.defaultTopK).toBe(15); }); }); describe('Semantic Search', () => { it('should perform semantic search', async () => { const results = await service.semanticSearch('用户偏好'); expect(mockVikingClient.find).toHaveBeenCalled(); expect(results.length).toBeGreaterThan(0); expect(results[0].score).toBeGreaterThanOrEqual(0); }); it('should respect topK option', async () => { await service.semanticSearch('测试', { topK: 5 }); expect(mockVikingClient.find).toHaveBeenCalledWith( '测试', expect.objectContaining({ limit: 5 }) ); }); it('should respect minScore option', async () => { await service.semanticSearch('测试', { minScore: 0.8 }); expect(mockVikingClient.find).toHaveBeenCalledWith( '测试', expect.objectContaining({ minScore: 0.8 }) ); }); it('should respect level option', async () => { await service.semanticSearch('测试', { level: 'L2' }); expect(mockVikingClient.find).toHaveBeenCalledWith( '测试', expect.objectContaining({ level: 'L2' }) ); }); it('should return empty array when disabled', async () => { service.updateConfig({ enabled: false }); const results = await service.semanticSearch('测试'); expect(results).toEqual([]); }); it('should filter by types when specified', async () => { const results = await service.semanticSearch('用户偏好', { types: ['preference'] }); // Should only return preference type memories for (const result of results) { expect(result.memory.type).toBe('preference'); } }); }); describe('Find Similar', () => { it('should find similar memories', async () => { const results = await service.findSimilar('memory1', { agentId: 'agent1' }); expect(mockMemoryManager.getAll).toHaveBeenCalledWith('agent1'); expect(mockVikingClient.find).toHaveBeenCalled(); }); it('should return empty array for non-existent memory', async () => { mockMemoryManager.getAll.mockResolvedValueOnce([]); const results = await service.findSimilar('non-existent', { agentId: 'agent1' }); expect(results).toEqual([]); }); }); describe('Find By Concept', () => { it('should find memories by concept', async () => { const results = await service.findByConcept('代码优化'); expect(mockVikingClient.find).toHaveBeenCalledWith( '代码优化', expect.any(Object) ); expect(results.length).toBeGreaterThanOrEqual(0); }); }); describe('Clustering', () => { it('should cluster memories', async () => { const clusters = await service.clusterMemories('agent1', 3); expect(mockMemoryManager.getAll).toHaveBeenCalledWith('agent1'); expect(Array.isArray(clusters)).toBe(true); }); it('should return empty array for agent with no memories', async () => { mockMemoryManager.getAll.mockResolvedValueOnce([]); const clusters = await service.clusterMemories('empty-agent'); expect(clusters).toEqual([]); }); }); describe('Availability', () => { it('should check availability', async () => { const available = await service.isAvailable(); expect(available).toBe(true); }); it('should return false when disabled', async () => { service.updateConfig({ enabled: false }); const available = await service.isAvailable(); expect(available).toBe(false); }); }); describe('Cache', () => { it('should clear cache', () => { service.clearCache(); // No error means success expect(true).toBe(true); }); }); }); // === Helper Function Tests === describe('Helper Functions', () => { beforeEach(() => { vi.clearAllMocks(); resetVectorMemory(); }); afterEach(() => { resetVectorMemory(); }); describe('getVectorMemory', () => { it('should return singleton instance', () => { const instance1 = getVectorMemory(); const instance2 = getVectorMemory(); expect(instance1).toBe(instance2); }); }); describe('semanticSearch helper', () => { it('should call service.semanticSearch', async () => { const results = await semanticSearch('测试查询'); expect(mockVikingClient.find).toHaveBeenCalled(); expect(Array.isArray(results)).toBe(true); }); }); describe('findSimilarMemories helper', () => { it('should call service.findSimilar', async () => { const results = await findSimilarMemories('memory1', 'agent1'); expect(mockMemoryManager.getAll).toHaveBeenCalled(); expect(Array.isArray(results)).toBe(true); }); }); describe('isVectorSearchAvailable helper', () => { it('should call service.isAvailable', async () => { const available = await isVectorSearchAvailable(); expect(typeof available).toBe('boolean'); }); }); }); // === Integration Tests === describe('VectorMemoryService Integration', () => { it('should handle Viking client errors gracefully', async () => { mockVikingClient.find.mockRejectedValueOnce(new Error('Connection failed')); const service = new VectorMemoryService(); const results = await service.semanticSearch('测试'); expect(results).toEqual([]); }); it('should handle missing Viking client gracefully', async () => { vi.mocked(getVikingClient).mockImplementation(() => { throw new Error('Viking not available'); }); const service = new VectorMemoryService(); const results = await service.semanticSearch('测试'); expect(results).toEqual([]); }); });