320 lines
13 KiB
JavaScript
320 lines
13 KiB
JavaScript
/**
|
|
* 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
|