/** * Tests for BetterSqlite3Backend with FTS5 Trigram Tokenizer for CJK Support * * These tests verify proper CJK (Japanese, Chinese, Korean) language support * using the native SQLite trigram tokenizer. */ import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest'; import { BetterSqlite3Backend, createBetterSqlite3Backend, createJapaneseOptimizedBackend } from '../better-sqlite3-backend.js'; import type { MemoryEntry } from '../types.js'; // Skip tests if better-sqlite3 is not available let betterSqlite3Available = false; try { await import('better-sqlite3'); betterSqlite3Available = true; } catch { console.log('[Test] better-sqlite3 not available, skipping native tests'); } const describeCond = betterSqlite3Available ? describe : describe.skip; describeCond('BetterSqlite3Backend', () => { let backend: BetterSqlite3Backend; beforeEach(async () => { backend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'trigram', verbose: false, }); await backend.initialize(); }); afterEach(async () => { await backend.shutdown(); }); describe('initialization', () => { it('should initialize successfully', async () => { expect(backend).toBeDefined(); }); it('should have FTS5 available', () => { expect(backend.isFtsAvailable()).toBe(true); }); it('should use trigram tokenizer', () => { expect(backend.getActiveTokenizer()).toBe('trigram'); }); it('should report CJK optimized', () => { expect(backend.isCjkOptimized()).toBe(true); }); it('should pass health check with CJK support', async () => { const health = await backend.healthCheck(); expect(health.status).toBe('healthy'); // index component shows tokenizer (Trigram for CJK support) expect(health.components.index.status).toBe('healthy'); expect(health.components.index.message).toContain('trigram'); // cache component shows vector search status expect(health.components.cache.status).toBe('healthy'); expect(health.components.cache.message).toContain('sqlite-vec'); }); }); describe('basic CRUD operations', () => { it('should store and retrieve entries', async () => { const entry: MemoryEntry = { id: 'test-1', key: 'test-key', content: 'Test content', type: 'semantic', namespace: 'default', tags: ['test'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); const retrieved = await backend.get('test-1'); expect(retrieved).not.toBeNull(); expect(retrieved?.id).toBe('test-1'); expect(retrieved?.content).toBe('Test content'); }); it('should update entries', async () => { const entry: MemoryEntry = { id: 'test-update', key: 'original-key', content: 'Original content', type: 'semantic', namespace: 'default', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); const updated = await backend.update('test-update', { content: 'Updated content' }); expect(updated?.content).toBe('Updated content'); expect(updated?.version).toBe(2); }); it('should delete entries', async () => { const entry: MemoryEntry = { id: 'test-delete', key: 'delete-key', content: 'Delete me', type: 'semantic', namespace: 'default', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); const deleted = await backend.delete('test-delete'); const retrieved = await backend.get('test-delete'); expect(deleted).toBe(true); expect(retrieved).toBeNull(); }); }); describe('FTS5 with trigram tokenizer', () => { beforeEach(async () => { // Insert test entries const entries: MemoryEntry[] = [ { id: 'en-1', key: 'english', content: 'Authentication using JWT tokens with refresh mechanism', type: 'semantic', namespace: 'patterns', tags: ['auth'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, { id: 'en-2', key: 'database', content: 'PostgreSQL connection pooling for high performance', type: 'semantic', namespace: 'patterns', tags: ['database'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, ]; await backend.bulkInsert(entries); }); it('should find English entries by keyword', async () => { const results = await backend.searchFts('authentication'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'en-1')).toBe(true); }); it('should find entries by partial match', async () => { const results = await backend.searchFts('auth'); expect(results.length).toBeGreaterThan(0); }); it('should filter by namespace', async () => { const results = await backend.searchFts('authentication', { namespace: 'patterns' }); expect(results.every((r) => r.namespace === 'patterns')).toBe(true); }); }); describe('CJK language support', () => { describe('Japanese (日本語)', () => { beforeEach(async () => { const entries: MemoryEntry[] = [ { id: 'jp-1', key: 'japanese-1', content: '日本語のテスト内容です。認証機能の実装について説明します。', type: 'semantic', namespace: 'japanese', tags: ['日本語', 'テスト'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, { id: 'jp-2', key: 'japanese-2', content: 'データベース接続プーリングの実装パターン', type: 'semantic', namespace: 'japanese', tags: ['データベース'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, ]; await backend.bulkInsert(entries); }); it('should find entries by Japanese text', async () => { const results = await backend.searchFts('日本語'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'jp-1')).toBe(true); }); it('should find entries by Japanese partial text', async () => { // Trigram tokenizer needs 3+ characters for reliable matching const results = await backend.searchFts('認証機能'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'jp-1')).toBe(true); }); it('should find entries by katakana', async () => { const results = await backend.searchFts('データベース'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'jp-2')).toBe(true); }); it('should find entries by hiragana', async () => { // 'テスト内容' is a longer phrase that appears in the content const results = await backend.searchFts('テスト内容'); expect(results.length).toBeGreaterThan(0); }); }); describe('Chinese (中文)', () => { beforeEach(async () => { const entries: MemoryEntry[] = [ { id: 'cn-1', key: 'chinese-1', content: '中文测试内容。这是关于用户认证的说明。', type: 'semantic', namespace: 'chinese', tags: ['中文', '测试'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, { id: 'cn-2', key: 'chinese-2', content: '数据库连接池配置说明', type: 'semantic', namespace: 'chinese', tags: ['数据库'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, ]; await backend.bulkInsert(entries); }); it('should find entries by Chinese text', async () => { // Use 3+ character term for trigram tokenizer const results = await backend.searchFts('中文测试'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'cn-1')).toBe(true); }); it('should find entries by Chinese partial text', async () => { // Use longer phrase for reliable trigram matching const results = await backend.searchFts('用户认证'); expect(results.length).toBeGreaterThan(0); }); it('should find entries by Chinese database term', async () => { const results = await backend.searchFts('数据库'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'cn-2')).toBe(true); }); }); describe('Korean (한국어)', () => { beforeEach(async () => { const entries: MemoryEntry[] = [ { id: 'kr-1', key: 'korean-1', content: '한국어 테스트 내용입니다. 사용자 인증에 대한 설명입니다.', type: 'semantic', namespace: 'korean', tags: ['한국어', '테스트'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, { id: 'kr-2', key: 'korean-2', content: '데이터베이스 연결 풀 설정 방법', type: 'semantic', namespace: 'korean', tags: ['데이터베이스'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, ]; await backend.bulkInsert(entries); }); it('should find entries by Korean text', async () => { const results = await backend.searchFts('한국어'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'kr-1')).toBe(true); }); it('should find entries by Korean partial text', async () => { // Use longer phrase for reliable trigram matching const results = await backend.searchFts('사용자 인증'); expect(results.length).toBeGreaterThan(0); }); it('should find entries by Korean database term', async () => { const results = await backend.searchFts('데이터베이스'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'kr-2')).toBe(true); }); }); describe('Mixed language support', () => { beforeEach(async () => { const entry: MemoryEntry = { id: 'mixed-1', key: 'mixed', content: 'API設計パターン - Japanese API design patterns using REST and GraphQL', type: 'semantic', namespace: 'mixed', tags: ['API', '設計'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); }); it('should find by Japanese in mixed content', async () => { const results = await backend.searchFts('設計パターン'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'mixed-1')).toBe(true); }); it('should find by English in mixed content', async () => { const results = await backend.searchFts('GraphQL'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'mixed-1')).toBe(true); }); it('should find by API term in mixed content', async () => { const results = await backend.searchFts('API'); expect(results.length).toBeGreaterThan(0); expect(results.some((r) => r.id === 'mixed-1')).toBe(true); }); }); }); describe('edge cases', () => { it('should handle empty query', async () => { const results = await backend.searchFts(''); expect(results.length).toBe(0); }); it('should handle whitespace-only query', async () => { const results = await backend.searchFts(' '); expect(results.length).toBe(0); }); it('should handle special characters', async () => { const results = await backend.searchFts('test*[query]'); expect(Array.isArray(results)).toBe(true); }); it('should handle very long content', async () => { const longContent = '日本語テスト '.repeat(1000); const entry: MemoryEntry = { id: 'long-content', key: 'long', content: longContent, type: 'semantic', namespace: 'default', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); const results = await backend.searchFts('日本語'); expect(results.some((r) => r.id === 'long-content')).toBe(true); }); }); describe('bulk operations', () => { it('should handle bulk insert', async () => { const entries: MemoryEntry[] = Array.from({ length: 100 }, (_, i) => ({ id: `bulk-${i}`, key: `key-${i}`, content: `Bulk content ${i} with 日本語 and 中文`, type: 'semantic' as const, namespace: 'bulk', tags: [], metadata: {}, accessLevel: 'project' as const, createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), })); await backend.bulkInsert(entries); const count = await backend.count('bulk'); expect(count).toBe(100); // FTS should work on bulk inserted entries const results = await backend.searchFts('日本語', { namespace: 'bulk' }); expect(results.length).toBeGreaterThan(0); }); it('should handle bulk delete', async () => { const entries: MemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({ id: `delete-${i}`, key: `key-${i}`, content: `Delete content ${i}`, type: 'semantic' as const, namespace: 'delete', tags: [], metadata: {}, accessLevel: 'project' as const, createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), })); await backend.bulkInsert(entries); const deleted = await backend.bulkDelete(entries.slice(0, 5).map((e) => e.id)); const remaining = await backend.count('delete'); expect(deleted).toBe(5); expect(remaining).toBe(5); }); }); describe('statistics', () => { it('should return correct stats', async () => { const entries: MemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({ id: `stats-${i}`, key: `key-${i}`, content: `Stats content ${i}`, type: 'semantic' as const, namespace: i < 5 ? 'ns1' : 'ns2', tags: [], metadata: {}, accessLevel: 'project' as const, createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), })); await backend.bulkInsert(entries); const stats = await backend.getStats(); expect(stats.totalEntries).toBe(10); expect(stats.entriesByNamespace.ns1).toBe(5); expect(stats.entriesByNamespace.ns2).toBe(5); expect(stats.memoryUsage).toBeGreaterThan(0); }); }); }); describe('createBetterSqlite3Backend factory', () => { const describeCond = betterSqlite3Available ? describe : describe.skip; describeCond('factory function', () => { it('should create backend with default trigram tokenizer', async () => { const backend = createBetterSqlite3Backend({ databasePath: ':memory:', }); await backend.initialize(); expect(backend.getActiveTokenizer()).toBe('trigram'); expect(backend.isCjkOptimized()).toBe(true); await backend.shutdown(); }); it('should allow custom tokenizer', async () => { const backend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'unicode61', }); await backend.initialize(); expect(backend.getActiveTokenizer()).toBe('unicode61'); expect(backend.isCjkOptimized()).toBe(false); await backend.shutdown(); }); it('should rebuild FTS index', async () => { const backend = createBetterSqlite3Backend({ databasePath: ':memory:', }); await backend.initialize(); // Store an entry await backend.store({ id: 'rebuild-test', key: 'rebuild-key', content: 'Content for FTS rebuild test', type: 'semantic', namespace: 'test', tags: ['rebuild'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Rebuild should not throw await expect(backend.rebuildFtsIndex()).resolves.not.toThrow(); // Search should still work after rebuild const results = await backend.query({ type: 'keyword', content: 'rebuild' }); expect(results.length).toBeGreaterThan(0); await backend.shutdown(); }); it('should handle entries with embeddings', async () => { const backend = createBetterSqlite3Backend({ databasePath: ':memory:', }); await backend.initialize(); // Create an entry with embedding const embedding = new Float32Array(384); for (let i = 0; i < 384; i++) { embedding[i] = i / 384; } await backend.store({ id: 'emb-test', key: 'embedding-key', content: 'Content with vector embedding', type: 'semantic', namespace: 'embeddings', tags: ['vector'], metadata: { hasEmbedding: true }, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Retrieve and verify embedding is preserved const entry = await backend.get('emb-test'); expect(entry).not.toBeNull(); expect(entry!.embedding).toBeInstanceOf(Float32Array); expect(entry!.embedding!.length).toBe(384); expect(entry?.embedding?.[0]).toBeCloseTo(0, 5); expect(entry?.embedding?.[100]).toBeCloseTo(100 / 384, 5); expect(entry?.embedding?.[383]).toBeCloseTo(383 / 384, 5); await backend.shutdown(); }); it('should create Japanese optimized backend', () => { // Note: This test verifies configuration, not actual lindera loading // since lindera extension needs to be built separately const backend = createJapaneseOptimizedBackend({ databasePath: ':memory:', linderaPath: '/path/to/liblindera_sqlite.dylib', }); // Backend is created with the configuration expect(backend).toBeInstanceOf(BetterSqlite3Backend); // Note: initialization would fail without the actual extension file }); }); }); describeCond('BetterSqlite3Backend advanced', () => { let backend: BetterSqlite3Backend; beforeEach(async () => { backend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'trigram', verbose: false, }); await backend.initialize(); }); afterEach(async () => { await backend.shutdown(); }); describe('query filters', () => { beforeEach(async () => { const now = Date.now(); const entries: MemoryEntry[] = [ { id: 'old-entry', key: 'old', content: 'Old content', type: 'episodic', namespace: 'time-test', tags: ['old'], metadata: {}, accessLevel: 'project', createdAt: now - 100000, updatedAt: now - 100000, version: 1, references: [], accessCount: 0, lastAccessedAt: now - 100000, }, { id: 'new-entry', key: 'new', content: 'New content', type: 'semantic', namespace: 'time-test', tags: ['new'], metadata: {}, accessLevel: 'project', createdAt: now, updatedAt: now, version: 1, references: [], accessCount: 0, lastAccessedAt: now, }, ]; await backend.bulkInsert(entries); }); it('should filter by createdBefore', async () => { const results = await backend.query({ type: 'hybrid', namespace: 'time-test', createdBefore: Date.now() - 50000, limit: 10, }); expect(results.length).toBe(1); expect(results[0].id).toBe('old-entry'); }); it('should filter by createdAfter', async () => { const results = await backend.query({ type: 'hybrid', namespace: 'time-test', createdAfter: Date.now() - 50000, limit: 10, }); expect(results.length).toBe(1); expect(results[0].id).toBe('new-entry'); }); it('should filter by memoryType', async () => { const results = await backend.query({ type: 'hybrid', namespace: 'time-test', memoryType: 'episodic', limit: 10, }); expect(results.length).toBe(1); expect(results[0].id).toBe('old-entry'); }); it('should filter by tags', async () => { const results = await backend.query({ type: 'hybrid', namespace: 'time-test', tags: ['old'], limit: 10, }); expect(results.length).toBe(1); expect(results[0].id).toBe('old-entry'); }); it('should filter by multiple tags', async () => { await backend.store({ id: 'multi-tag', key: 'multi', content: 'Multi tag content', type: 'semantic', namespace: 'time-test', tags: ['old', 'new', 'special'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const results = await backend.query({ type: 'hybrid', namespace: 'time-test', tags: ['special'], limit: 10, }); expect(results.length).toBe(1); expect(results[0].id).toBe('multi-tag'); }); }); describe('getByKey', () => { it('should retrieve entry by namespace and key', async () => { await backend.store({ id: 'key-test-1', key: 'unique-key', content: 'Content by key', type: 'semantic', namespace: 'key-ns', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const result = await backend.getByKey('key-ns', 'unique-key'); expect(result).not.toBeNull(); expect(result?.id).toBe('key-test-1'); }); it('should return null for non-existent key', async () => { const result = await backend.getByKey('non-existent-ns', 'non-existent-key'); expect(result).toBeNull(); }); it('should increment access count on getByKey', async () => { await backend.store({ id: 'access-test', key: 'access-key', content: 'Access content', type: 'semantic', namespace: 'access-ns', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.getByKey('access-ns', 'access-key'); const result = await backend.getByKey('access-ns', 'access-key'); expect(result?.accessCount).toBeGreaterThanOrEqual(1); }); }); describe('semantic search', () => { let vecBackend: BetterSqlite3Backend; beforeEach(async () => { // Create separate backend with 8-dimension vectors for these tests vecBackend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'trigram', verbose: false, vectorDimensions: 8, }); await vecBackend.initialize(); }); afterEach(async () => { await vecBackend.shutdown(); }); it('should perform vector search with embeddings', async () => { // Verify sqlite-vec is available expect(vecBackend.isVectorAvailable()).toBe(true); // Create distinct vectors - embedding1 is similar to query, embedding2 is different const embedding1 = new Float32Array([1, 0, 0, 0, 0, 0, 0, 0]); const embedding2 = new Float32Array([0, 1, 0, 0, 0, 0, 0, 0]); await vecBackend.store({ id: 'vec-1', key: 'vector-1', content: 'Vector content 1', type: 'semantic', namespace: 'vectors', tags: [], metadata: {}, embedding: embedding1, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await vecBackend.store({ id: 'vec-2', key: 'vector-2', content: 'Vector content 2', type: 'semantic', namespace: 'vectors', tags: [], metadata: {}, embedding: embedding2, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Query similar to embedding1 const queryEmbedding = new Float32Array([1, 0, 0, 0, 0, 0, 0, 0]); const results = await vecBackend.search(queryEmbedding, { k: 2 }); expect(results.length).toBe(2); // First result should be vec-1 (identical to query) expect(results[0].entry.id).toBe('vec-1'); expect(results[0].score).toBeCloseTo(1, 5); // Identical vectors }); it('should apply namespace filter in vector search', async () => { const embedding = new Float32Array(8).fill(0.5); await vecBackend.store({ id: 'vec-ns1', key: 'vector-ns1', content: 'Content 1', type: 'semantic', namespace: 'ns1', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await vecBackend.store({ id: 'vec-ns2', key: 'vector-ns2', content: 'Content 2', type: 'semantic', namespace: 'ns2', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const results = await vecBackend.search(embedding, { k: 10, filters: { namespace: 'ns1' }, }); expect(results.length).toBe(1); expect(results[0].entry.namespace).toBe('ns1'); }); it('should apply memoryType filter in vector search', async () => { const embedding = new Float32Array(8).fill(0.5); await vecBackend.store({ id: 'vec-type1', key: 'vector-type1', content: 'Content 1', type: 'episodic', namespace: 'types', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await vecBackend.store({ id: 'vec-type2', key: 'vector-type2', content: 'Content 2', type: 'semantic', namespace: 'types', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const results = await vecBackend.search(embedding, { k: 10, filters: { memoryType: 'episodic' }, }); expect(results.length).toBe(1); expect(results[0].entry.type).toBe('episodic'); }); it('should apply threshold filter in vector search', async () => { const embedding1 = new Float32Array(8).fill(1); const embedding2 = new Float32Array(8).fill(-1); await vecBackend.store({ id: 'vec-sim', key: 'similar', content: 'Similar content', type: 'semantic', namespace: 'threshold', tags: [], metadata: {}, embedding: embedding1, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await vecBackend.store({ id: 'vec-diff', key: 'different', content: 'Different content', type: 'semantic', namespace: 'threshold', tags: [], metadata: {}, embedding: embedding2, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const queryEmbedding = new Float32Array(8).fill(1); const results = await vecBackend.search(queryEmbedding, { k: 10, threshold: 0.9, // High threshold should filter out dissimilar }); expect(results.length).toBe(1); expect(results[0].entry.id).toBe('vec-sim'); }); it('should store entry but skip vector when dimension mismatch', async () => { const embedding1 = new Float32Array(8).fill(0.5); const embedding2 = new Float32Array(16).fill(0.5); // Different size - will be skipped in vec table await vecBackend.store({ id: 'vec-size1', key: 'size1', content: 'Content 1', type: 'semantic', namespace: 'sizes', tags: [], metadata: {}, embedding: embedding1, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Mismatched dimension vector - entry stored but vector insertion skipped await vecBackend.store({ id: 'vec-size2', key: 'size2', content: 'Content 2', type: 'semantic', namespace: 'sizes', tags: [], metadata: {}, embedding: embedding2, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const queryEmbedding = new Float32Array(8).fill(0.5); const results = await vecBackend.search(queryEmbedding, { k: 10 }); // Only the correctly sized vector should be found via vector search expect(results.length).toBe(1); expect(results[0].entry.id).toBe('vec-size1'); }); }); describe('namespace operations', () => { it('should list all namespaces', async () => { await backend.store({ id: 'ns-test-1', key: 'key1', content: 'Content 1', type: 'semantic', namespace: 'namespace-a', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'ns-test-2', key: 'key2', content: 'Content 2', type: 'semantic', namespace: 'namespace-b', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const namespaces = await backend.listNamespaces(); expect(namespaces).toContain('namespace-a'); expect(namespaces).toContain('namespace-b'); }); it('should clear namespace', async () => { await backend.store({ id: 'clear-1', key: 'key1', content: 'Content 1', type: 'semantic', namespace: 'to-clear', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'clear-2', key: 'key2', content: 'Content 2', type: 'semantic', namespace: 'to-keep', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const cleared = await backend.clearNamespace('to-clear'); expect(cleared).toBe(1); const remainingCleared = await backend.count('to-clear'); const remainingKept = await backend.count('to-keep'); expect(remainingCleared).toBe(0); expect(remainingKept).toBe(1); }); }); describe('update operations', () => { it('should return null when updating non-existent entry', async () => { const result = await backend.update('non-existent-id', { content: 'New content' }); expect(result).toBeNull(); }); it('should update multiple fields', async () => { await backend.store({ id: 'update-test', key: 'update-key', content: 'Original content', type: 'semantic', namespace: 'update-ns', tags: ['original'], metadata: { original: true }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const updated = await backend.update('update-test', { content: 'Updated content', tags: ['updated'], metadata: { updated: true }, }); expect(updated?.content).toBe('Updated content'); expect(updated?.tags).toContain('updated'); expect(updated?.metadata.updated).toBe(true); expect(updated?.version).toBe(2); }); }); describe('health check scenarios', () => { it('should report healthy status with different tokenizers', async () => { // Create backend with unicode tokenizer (not CJK optimized) const unicodeBackend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'unicode61', }); await unicodeBackend.initialize(); const health = await unicodeBackend.healthCheck(); // FTS is still available, just not CJK optimized expect(health.components.index.status).toBe('healthy'); expect(health.components.index.message).toContain('unicode61'); // sqlite-vec should be healthy expect(health.components.cache.status).toBe('healthy'); expect(health.components.cache.message).toContain('sqlite-vec'); await unicodeBackend.shutdown(); }); }); describe('getDatabase', () => { it('should return the underlying database', () => { const db = backend.getDatabase(); expect(db).not.toBeNull(); }); }); describe('events', () => { it('should emit entry:stored event', async () => { const events: unknown[] = []; backend.on('entry:stored', (data) => events.push(data)); await backend.store({ id: 'event-test', key: 'event-key', content: 'Event content', type: 'semantic', namespace: 'events', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); expect(events.length).toBe(1); }); it('should emit bulkInserted event', async () => { const events: unknown[] = []; backend.on('bulkInserted', (count) => events.push(count)); await backend.bulkInsert([ { id: 'bulk-event-1', key: 'bulk-1', content: 'Bulk content 1', type: 'semantic', namespace: 'events', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, { id: 'bulk-event-2', key: 'bulk-2', content: 'Bulk content 2', type: 'semantic', namespace: 'events', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }, ]); expect(events).toContain(2); }); }); describe('verbose mode', () => { it('should log when verbose is enabled', async () => { const verboseBackend = createBetterSqlite3Backend({ databasePath: ':memory:', verbose: true, }); // Capture console.log const logs: string[] = []; const originalLog = console.log; console.log = (...args) => logs.push(args.join(' ')); await verboseBackend.initialize(); await verboseBackend.rebuildFtsIndex(); await verboseBackend.shutdown(); console.log = originalLog; expect(logs.some((l) => l.includes('[BetterSqlite3]'))).toBe(true); }); }); describe('porter tokenizer', () => { it('should support porter tokenizer', async () => { const porterBackend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'porter', }); await porterBackend.initialize(); expect(porterBackend.getActiveTokenizer()).toBe('porter'); expect(porterBackend.isFtsAvailable()).toBe(true); // Porter should work for English stemming await porterBackend.store({ id: 'porter-1', key: 'running', content: 'The quick brown fox is running', type: 'semantic', namespace: 'porter', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Porter stemmer should match "run" to "running" const results = await porterBackend.searchFts('run'); expect(results.length).toBeGreaterThan(0); await porterBackend.shutdown(); }); }); describe('empty bulk operations', () => { it('should handle empty bulk insert', async () => { await backend.bulkInsert([]); expect(await backend.count()).toBe(0); }); it('should handle empty bulk delete', async () => { const deleted = await backend.bulkDelete([]); expect(deleted).toBe(0); }); }); describe('custom tokenizer', () => { it('should report custom tokenizer when configured', async () => { const customBackend = createBetterSqlite3Backend({ databasePath: ':memory:', customTokenizer: 'custom_tok', }); // Note: This will fail to create FTS since custom_tok doesn't exist, // but the tokenizer name should still be reported expect(customBackend.getActiveTokenizer()).toBe('custom_tok'); }); }); describe('double initialization', () => { it('should handle double initialization', async () => { const newBackend = createBetterSqlite3Backend({ databasePath: ':memory:', }); await newBackend.initialize(); await newBackend.initialize(); // Should not throw expect(newBackend.isFtsAvailable()).toBe(true); await newBackend.shutdown(); }); }); describe('query with no filters', () => { it('should return all entries with no filters', async () => { await backend.store({ id: 'no-filter-1', key: 'key1', content: 'Content 1', type: 'semantic', namespace: 'ns1', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'no-filter-2', key: 'key2', content: 'Content 2', type: 'episodic', namespace: 'ns2', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const results = await backend.query({ type: 'hybrid', limit: 10 }); expect(results.length).toBe(2); }); }); describe('delete non-existent', () => { it('should return false when deleting non-existent entry', async () => { const result = await backend.delete('non-existent-id'); expect(result).toBe(false); }); }); /** * STRICT TESTS - Bug Catchers for Backend */ describe('Strict: SQL Injection Prevention', () => { const injectionPayloads = [ "'; DROP TABLE memories; --", "1'; DELETE FROM memories WHERE '1'='1", "Robert'); DROP TABLE memories;--", "1 OR 1=1", "' UNION SELECT * FROM memories --", ]; injectionPayloads.forEach((payload, idx) => { it(`should safely handle SQL injection attempt #${idx + 1}`, async () => { const entry: MemoryEntry = { id: `injection-test-${idx}`, key: payload, content: payload, type: 'semantic', namespace: payload, tags: [payload], metadata: { payload }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); const retrieved = await backend.get(`injection-test-${idx}`); expect(retrieved).not.toBeNull(); expect(retrieved!.content).toBe(payload); expect(retrieved!.key).toBe(payload); // Verify the database still works const count = await backend.count(); expect(count).toBeGreaterThan(0); }); }); }); describe('Strict: Data Integrity Under Stress', () => { it('should handle rapid sequential writes correctly', async () => { for (let i = 0; i < 100; i++) { await backend.store({ id: `rapid-${i}`, key: `key-${i}`, content: `Content ${i}`, type: 'semantic', namespace: 'stress-test', tags: [`tag-${i % 10}`], metadata: { index: i }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); } const count = await backend.count('stress-test'); expect(count).toBe(100); // Verify random entries are intact for (const idx of [0, 25, 50, 75, 99]) { const retrieved = await backend.get(`rapid-${idx}`); expect(retrieved!.content).toBe(`Content ${idx}`); expect(retrieved!.metadata).toEqual({ index: idx }); } }); it('should handle bulk insert correctly', async () => { const entries: MemoryEntry[] = Array.from({ length: 50 }, (_, i) => ({ id: `bulk-${i}`, key: `bulk-key-${i}`, content: `Bulk content ${i}`, type: 'semantic' as const, namespace: 'bulk-test', tags: [], metadata: {}, accessLevel: 'project' as const, createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), })); await backend.bulkInsert(entries); const count = await backend.count('bulk-test'); expect(count).toBe(50); }); }); describe('Strict: FTS5 Search Accuracy', () => { beforeEach(async () => { const entries = [ { id: 'fts-1', content: 'React hooks useEffect useState' }, { id: 'fts-2', content: 'Vue composition API reactive ref' }, { id: 'fts-3', content: 'Angular dependency injection services' }, { id: 'fts-4', content: 'React Native mobile development iOS Android' }, { id: 'fts-5', content: 'Node.js Express backend API REST' }, ]; for (const e of entries) { await backend.store({ id: e.id, key: e.id, content: e.content, type: 'semantic', namespace: 'fts-test', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); } }); it('should find entries containing search term', async () => { const results = await backend.searchFts('React', { namespace: 'fts-test', limit: 10 }); expect(results.length).toBeGreaterThanOrEqual(1); const reactResults = results.filter((r) => r.content.toLowerCase().includes('react')); expect(reactResults.length).toBeGreaterThanOrEqual(1); }); it('should return results for exact word match', async () => { const results = await backend.searchFts('Angular', { namespace: 'fts-test', limit: 10 }); expect(results.length).toBeGreaterThanOrEqual(1); expect(results[0].content).toContain('Angular'); }); }); describe('Strict: Update Edge Cases', () => { it('should return null when updating non-existent entry', async () => { const result = await backend.update('non-existent-id', { content: 'New content' }); expect(result).toBeNull(); }); it('should update only specified fields', async () => { const original: MemoryEntry = { id: 'update-fields-test', key: 'original-key', content: 'Original content', type: 'semantic', namespace: 'test-ns', tags: ['original-tag'], metadata: { original: true }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: ['ref-1'], accessCount: 5, lastAccessedAt: Date.now(), }; await backend.store(original); const updated = await backend.update('update-fields-test', { content: 'Updated content', }); expect(updated!.content).toBe('Updated content'); expect(updated!.key).toBe('original-key'); expect(updated!.tags).toEqual(['original-tag']); expect(updated!.metadata).toEqual({ original: true }); expect(updated!.version).toBe(2); }); }); describe('Strict: Namespace Isolation', () => { it('should correctly count entries per namespace', async () => { await backend.store({ id: 'ns-iso-1', key: 'k1', content: 'c1', type: 'semantic', namespace: 'namespace-a', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'ns-iso-2', key: 'k2', content: 'c2', type: 'semantic', namespace: 'namespace-a', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'ns-iso-3', key: 'k3', content: 'c3', type: 'semantic', namespace: 'namespace-b', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); expect(await backend.count('namespace-a')).toBe(2); expect(await backend.count('namespace-b')).toBe(1); expect(await backend.count()).toBe(3); }); it('should clear only target namespace', async () => { await backend.store({ id: 'clear-1', key: 'k1', content: 'c1', type: 'semantic', namespace: 'to-clear', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'clear-2', key: 'k2', content: 'c2', type: 'semantic', namespace: 'to-keep', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const cleared = await backend.clearNamespace('to-clear'); expect(cleared).toBe(1); expect(await backend.count('to-clear')).toBe(0); expect(await backend.count('to-keep')).toBe(1); }); }); describe('Strict: Statistics Accuracy', () => { it('should report accurate statistics', async () => { await backend.store({ id: 'stat-1', key: 'k1', content: 'c1', type: 'semantic', namespace: 'stats-ns', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await backend.store({ id: 'stat-2', key: 'k2', content: 'c2', type: 'episodic', namespace: 'stats-ns', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const stats = await backend.getStats(); expect(stats.totalEntries).toBe(2); expect(stats.entriesByNamespace['stats-ns']).toBe(2); expect(stats.entriesByType.semantic).toBe(1); expect(stats.entriesByType.episodic).toBe(1); }); }); describe('Strict: Vector Search with sqlite-vec', () => { let vecBackend: BetterSqlite3Backend; beforeEach(async () => { vecBackend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'trigram', verbose: false, vectorDimensions: 8, }); await vecBackend.initialize(); }); afterEach(async () => { await vecBackend.shutdown(); }); it('should return results in similarity order', async () => { const queryVec = new Float32Array([1, 0, 0, 0, 0, 0, 0, 0]); const similarVec = new Float32Array([0.9, 0.1, 0, 0, 0, 0, 0, 0]); const differentVec = new Float32Array([0, 0, 0, 0, 0, 0, 0, 1]); await vecBackend.store({ id: 'similar', key: 'similar', content: 'Similar content', type: 'semantic', namespace: 'order-test', tags: [], metadata: {}, embedding: similarVec, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); await vecBackend.store({ id: 'different', key: 'different', content: 'Different content', type: 'semantic', namespace: 'order-test', tags: [], metadata: {}, embedding: differentVec, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); const results = await vecBackend.search(queryVec, { k: 2 }); expect(results.length).toBe(2); expect(results[0].entry.id).toBe('similar'); expect(results[0].score).toBeGreaterThan(results[1].score); }); it('should handle concurrent vector operations', async () => { const promises = Array.from({ length: 20 }, (_, i) => { const vec = new Float32Array(8).fill(i / 20); return vecBackend.store({ id: `concurrent-vec-${i}`, key: `key-${i}`, content: `Content ${i}`, type: 'semantic', namespace: 'concurrent-test', tags: [], metadata: {}, embedding: vec, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); }); await Promise.all(promises); const count = await vecBackend.count('concurrent-test'); expect(count).toBe(20); const queryVec = new Float32Array(8).fill(0.5); const results = await vecBackend.search(queryVec, { k: 5 }); expect(results.length).toBe(5); }); }); // ==================== STRICT: Concurrent Write Conflict Tests ==================== describe('Strict: Concurrent Write Conflicts', () => { it('should handle concurrent updates without data loss', async () => { // Store initial entry const entry: MemoryEntry = { id: 'conflict-test', key: 'conflict-key', content: 'version 1', type: 'factual', namespace: 'conflict-test', tags: ['conflict'], metadata: { version: 1 }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); // Attempt concurrent updates const update1 = backend.update('conflict-test', { content: 'version 2' }); const update2 = backend.update('conflict-test', { content: 'version 3' }); // Both should complete without throwing const results = await Promise.allSettled([update1, update2]); expect(results.every(r => r.status === 'fulfilled')).toBe(true); // Final state should reflect one of the updates (last write wins) const final = await backend.get('conflict-test'); expect(final).not.toBeNull(); expect(['version 2', 'version 3']).toContain(final!.content); }); it('should handle concurrent store and delete without crash', async () => { // Store initial entry const entry: MemoryEntry = { id: 'store-delete-conflict', key: 'sd-key', content: 'to be deleted', type: 'factual', namespace: 'conflict-test', tags: [], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); // Concurrent delete and re-store with same ID const newEntry = { ...entry, content: 'new content' }; const deleteOp = backend.delete('store-delete-conflict'); const storeOp = backend.store(newEntry); // Both should complete without throwing const results = await Promise.allSettled([deleteOp, storeOp]); expect(results.every(r => r.status === 'fulfilled')).toBe(true); // Entry may or may not exist, but system should be stable const final = await backend.get('store-delete-conflict'); if (final !== null) { expect(typeof final.content).toBe('string'); } }); it('should handle rapid sequential updates correctly', async () => { const entry: MemoryEntry = { id: 'rapid-update-test', key: 'rapid-key', content: 'initial', type: 'factual', namespace: 'rapid-test', tags: [], metadata: { counter: 0 }, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; await backend.store(entry); // Perform 50 rapid updates sequentially for (let i = 1; i <= 50; i++) { await backend.update('rapid-update-test', { content: `update ${i}`, metadata: { counter: i }, }); } // Final state should reflect last update const final = await backend.get('rapid-update-test'); expect(final).not.toBeNull(); expect(final!.content).toBe('update 50'); expect(final!.metadata?.counter).toBe(50); }); it('should maintain index consistency during concurrent writes', async () => { // Create multiple entries concurrently with same key pattern const promises = []; for (let i = 0; i < 10; i++) { const entry: MemoryEntry = { id: `index-conflict-${i}`, key: `shared-key-${i % 3}`, // Some keys will conflict content: `Content for index ${i}`, type: 'factual', namespace: 'index-conflict-test', tags: ['indexed'], metadata: {}, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }; promises.push(backend.store(entry)); } await Promise.all(promises); // All entries should be searchable via FTS const results = await backend.searchFts('Content index', { limit: 20 }); expect(results.length).toBeGreaterThanOrEqual(1); // Each entry should be retrievable by ID for (let i = 0; i < 10; i++) { const entry = await backend.get(`index-conflict-${i}`); expect(entry).not.toBeNull(); expect(entry!.id).toBe(`index-conflict-${i}`); } }); }); // ==================== Migration Tests ==================== describe('Migration: Embeddings to sqlite-vec', () => { let vecBackend: BetterSqlite3Backend; beforeEach(async () => { // Create backend with matching vector dimensions vecBackend = createBetterSqlite3Backend({ databasePath: ':memory:', ftsTokenizer: 'trigram', verbose: false, vectorDimensions: 8, // Match test embedding dimensions }); await vecBackend.initialize(); }); afterEach(async () => { await vecBackend.shutdown(); }); it('should report no migration needed for new database', async () => { const result = await vecBackend.needsMigration(); expect(result.needed).toBe(false); expect(result.count).toBe(0); }); it('should store entries with embeddings in sqlite-vec automatically', async () => { // Store entry with matching embedding dimensions const embedding = new Float32Array(8).fill(0.5); await vecBackend.store({ id: 'migrate-test-1', key: 'key-1', content: 'Content to migrate', type: 'factual', namespace: 'migration-test', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Entry should be already in sqlite-vec (no migration needed) const beforeMigration = await vecBackend.needsMigration(); expect(beforeMigration.needed).toBe(false); // Run migration (should be no-op since entry is already indexed) const result = await vecBackend.migrateEmbeddingsToVec(); expect(result.migrated).toBe(0); expect(result.errors).toBe(0); }); it('should return migration stats correctly', async () => { const result = await vecBackend.migrateEmbeddingsToVec(); expect(typeof result.migrated).toBe('number'); expect(typeof result.skipped).toBe('number'); expect(typeof result.errors).toBe('number'); }); it('should be searchable via vector after store', async () => { const embedding = new Float32Array(8).fill(0.5); await vecBackend.store({ id: 'vec-search-test', key: 'vec-key', content: 'Vector searchable content', type: 'factual', namespace: 'vec-test', tags: [], metadata: {}, embedding, accessLevel: 'project', createdAt: Date.now(), updatedAt: Date.now(), version: 1, references: [], accessCount: 0, lastAccessedAt: Date.now(), }); // Search with similar vector const queryVec = new Float32Array(8).fill(0.5); const results = await vecBackend.search(queryVec, { k: 5 }); expect(results.length).toBe(1); expect(results[0].entry.id).toBe('vec-search-test'); }); }); });