Major type system refactoring and error fixes across the codebase: **Type System Improvements:** - Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types - Added GatewayPong interface for WebSocket pong responses - Added index signature to MemorySearchOptions for Record compatibility - Fixed RawApproval interface with hand_name, run_id properties **Gateway & Protocol Fixes:** - Fixed performHandshake nonce handling in gateway-client.ts - Fixed onAgentStream callback type definitions - Fixed HandRun runId mapping to handle undefined values - Fixed Approval mapping with proper default values **Memory System Fixes:** - Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount) - Replaced getByAgent with getAll method in vector-memory.ts - Fixed MemorySearchOptions type compatibility **Component Fixes:** - Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent) - Fixed SkillMarket suggestSkills async call arguments - Fixed message-virtualization useRef generic type - Fixed session-persistence messageCount type conversion **Code Cleanup:** - Removed unused imports and variables across multiple files - Consolidated StoredError interface (removed duplicate) - Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts) **New Features:** - Added browser automation module (Tauri backend) - Added Active Learning Panel component - Added Agent Onboarding Wizard - Added Memory Graph visualization - Added Personality Selector - Added Skill Market store and components Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
300 lines
9.8 KiB
TypeScript
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([]);
|
|
});
|
|
});
|