/** * Tests for HybridBackend (ADR-009) * * Verifies that the hybrid backend correctly routes queries between * SQLite (structured) and AgentDB (semantic) backends. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { HybridBackend } from './hybrid-backend.js'; import { createDefaultEntry } from './types.js'; describe('HybridBackend - ADR-009', () => { let backend; // Mock embedding generator for testing const mockEmbedding = async (text) => { // Simple mock: convert text to numbers const arr = new Float32Array(128); for (let i = 0; i < Math.min(text.length, 128); i++) { arr[i] = text.charCodeAt(i) / 255; } return arr; }; beforeEach(async () => { backend = new HybridBackend({ sqlite: { databasePath: ':memory:', // In-memory for testing verbose: false, }, agentdb: { vectorDimension: 128, }, embeddingGenerator: mockEmbedding, dualWrite: true, }); await backend.initialize(); }); afterEach(async () => { await backend.shutdown(); }); describe('Initialization', () => { it('should initialize both backends', async () => { const health = await backend.healthCheck(); expect(health.status).toBe('healthy'); expect(health.components.storage).toBeDefined(); expect(health.components.index).toBeDefined(); expect(health.components.cache).toBeDefined(); }); }); describe('Store Operations', () => { it('should store entries in both backends (dual-write)', async () => { const entry = createDefaultEntry({ key: 'test-key', content: 'Test content for dual write', namespace: 'test', }); await backend.store(entry); // Verify in SQLite const fromSQLite = await backend.getSQLiteBackend().get(entry.id); expect(fromSQLite).toBeDefined(); expect(fromSQLite?.key).toBe('test-key'); // Verify in AgentDB const fromAgentDB = await backend.getAgentDBBackend().get(entry.id); expect(fromAgentDB).toBeDefined(); expect(fromAgentDB?.key).toBe('test-key'); }); it('should handle bulk inserts', async () => { const entries = Array.from({ length: 10 }, (_, i) => createDefaultEntry({ key: `bulk-${i}`, content: `Bulk content ${i}`, namespace: 'bulk-test', })); await backend.bulkInsert(entries); const count = await backend.count('bulk-test'); expect(count).toBe(10); }); }); describe('Exact Match Queries (SQLite)', () => { beforeEach(async () => { // Insert test data await backend.store(createDefaultEntry({ key: 'user-123', content: 'User data for testing', namespace: 'users', })); }); it('should route exact key queries to SQLite', async () => { const result = await backend.getByKey('users', 'user-123'); expect(result).toBeDefined(); expect(result?.key).toBe('user-123'); }); it('should handle prefix queries via SQLite', async () => { await backend.store(createDefaultEntry({ key: 'user-456', content: 'Another user', namespace: 'users', })); const results = await backend.queryStructured({ namespace: 'users', keyPrefix: 'user-', limit: 10, }); expect(results.length).toBeGreaterThanOrEqual(2); expect(results.every((r) => r.key.startsWith('user-'))).toBe(true); }); }); describe('Semantic Search (AgentDB)', () => { beforeEach(async () => { // Insert test data with semantic content await backend.store(createDefaultEntry({ key: 'doc-1', content: 'Authentication and authorization patterns', namespace: 'docs', tags: ['security', 'auth'], })); await backend.store(createDefaultEntry({ key: 'doc-2', content: 'Database optimization techniques', namespace: 'docs', tags: ['performance', 'database'], })); await backend.store(createDefaultEntry({ key: 'doc-3', content: 'Secure authentication methods', namespace: 'docs', tags: ['security', 'auth'], })); }); it('should perform semantic search via AgentDB', async () => { const results = await backend.querySemantic({ content: 'authentication security', k: 5, threshold: 0.1, // Low threshold for simple mock embeddings }); expect(results.length).toBeGreaterThan(0); // Should find docs about authentication const hasAuthDoc = results.some((r) => r.content.includes('Authentication')); expect(hasAuthDoc).toBe(true); }); it('should support semantic search with filters', async () => { const results = await backend.querySemantic({ content: 'security patterns', k: 10, filters: { type: 'semantic', tags: ['security'], limit: 10, }, }); expect(results.every((r) => r.tags.includes('security'))).toBe(true); }); }); describe('Hybrid Queries', () => { beforeEach(async () => { // Insert diverse test data for (let i = 0; i < 5; i++) { await backend.store(createDefaultEntry({ key: `hybrid-${i}`, content: `Content about ${i % 2 === 0 ? 'authentication' : 'database'} topic ${i}`, namespace: 'hybrid-test', tags: i % 2 === 0 ? ['auth'] : ['db'], })); } }); it('should combine semantic and structured queries (union)', async () => { const results = await backend.queryHybrid({ semantic: { content: 'authentication', k: 3, }, structured: { namespace: 'hybrid-test', keyPrefix: 'hybrid-', limit: 5, }, combineStrategy: 'union', }); expect(results.length).toBeGreaterThan(0); expect(results.every((r) => r.namespace === 'hybrid-test')).toBe(true); }); it('should support semantic-first strategy', async () => { const results = await backend.queryHybrid({ semantic: { content: 'database', k: 2, }, structured: { namespace: 'hybrid-test', limit: 3, }, combineStrategy: 'semantic-first', }); expect(results.length).toBeGreaterThan(0); // First results should be from semantic search }); }); describe('CRUD Operations', () => { let testEntry; beforeEach(async () => { testEntry = createDefaultEntry({ key: 'crud-test', content: 'Original content', namespace: 'test', }); await backend.store(testEntry); }); it('should update entries in both backends', async () => { const updated = await backend.update(testEntry.id, { content: 'Updated content', tags: ['updated'], }); expect(updated).toBeDefined(); expect(updated?.content).toBe('Updated content'); expect(updated?.tags).toContain('updated'); // Verify in SQLite const fromSQLite = await backend.getSQLiteBackend().get(testEntry.id); expect(fromSQLite?.content).toBe('Updated content'); // Verify in AgentDB const fromAgentDB = await backend.getAgentDBBackend().get(testEntry.id); expect(fromAgentDB?.content).toBe('Updated content'); }); it('should delete entries from both backends', async () => { const deleted = await backend.delete(testEntry.id); expect(deleted).toBe(true); const fromSQLite = await backend.getSQLiteBackend().get(testEntry.id); expect(fromSQLite).toBeNull(); const fromAgentDB = await backend.getAgentDBBackend().get(testEntry.id); expect(fromAgentDB).toBeNull(); }); }); describe('Namespace Operations', () => { beforeEach(async () => { await backend.store(createDefaultEntry({ key: 'ns1-key', content: 'Namespace 1 content', namespace: 'ns1', })); await backend.store(createDefaultEntry({ key: 'ns2-key', content: 'Namespace 2 content', namespace: 'ns2', })); }); it('should list all namespaces', async () => { const namespaces = await backend.listNamespaces(); expect(namespaces).toContain('ns1'); expect(namespaces).toContain('ns2'); }); it('should count entries per namespace', async () => { const ns1Count = await backend.count('ns1'); const ns2Count = await backend.count('ns2'); expect(ns1Count).toBe(1); expect(ns2Count).toBe(1); }); it('should clear namespace in both backends', async () => { const deleted = await backend.clearNamespace('ns1'); expect(deleted).toBe(1); const ns1Count = await backend.count('ns1'); expect(ns1Count).toBe(0); const ns2Count = await backend.count('ns2'); expect(ns2Count).toBe(1); }); }); describe('Statistics', () => { it('should provide combined statistics', async () => { // Add some test data for (let i = 0; i < 5; i++) { await backend.store(createDefaultEntry({ key: `stats-${i}`, content: `Stats content ${i}`, namespace: 'stats', })); } const stats = await backend.getStats(); expect(stats.totalEntries).toBeGreaterThanOrEqual(5); expect(stats.entriesByNamespace['stats']).toBe(5); expect(stats.hnswStats).toBeDefined(); expect(stats.cacheStats).toBeDefined(); }); }); describe('Health Check', () => { it('should report healthy status for both backends', async () => { const health = await backend.healthCheck(); expect(health.status).toBe('healthy'); expect(health.components.storage.status).toBe('healthy'); expect(health.components.index.status).toBe('healthy'); expect(health.components.cache.status).toBe('healthy'); }); }); describe('Query Routing', () => { it('should auto-route semantic queries to AgentDB', async () => { await backend.store(createDefaultEntry({ key: 'route-test', content: 'Routing test content', namespace: 'routing', })); const results = await backend.query({ type: 'semantic', content: 'routing test', limit: 5, }); // Verify we got some results expect(results).toBeDefined(); }); it('should auto-route exact queries to SQLite', async () => { await backend.store(createDefaultEntry({ key: 'exact-test', content: 'Exact match test', namespace: 'routing', })); const results = await backend.query({ type: 'exact', key: 'exact-test', namespace: 'routing', limit: 1, }); // Verify we got the exact result expect(results.length).toBeGreaterThan(0); expect(results[0].key).toBe('exact-test'); }); }); }); //# sourceMappingURL=hybrid-backend.test.js.map