Files
iven 1cf3f585d3 refactor(store): split gatewayStore into specialized domain stores
Major restructuring:
- Split monolithic gatewayStore into 5 focused stores:
  - connectionStore: WebSocket connection and gateway lifecycle
  - configStore: quickConfig, workspaceInfo, MCP services
  - agentStore: clones, usage stats, agent management
  - handStore: hands, approvals, triggers, hand runs
  - workflowStore: workflows, workflow runs, execution

- Update all components to use new stores with selector pattern
- Remove
2026-03-20 22:14:13 +08:00

300 lines
9.8 KiB
TypeScript

/**
* 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([]);
});
});