tasq/node_modules/@claude-flow/memory/dist/hybrid-backend.test.js

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