@aitytech/agentkits-memory 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -5
- package/dist/better-sqlite3-backend.d.ts +192 -0
- package/dist/better-sqlite3-backend.d.ts.map +1 -0
- package/dist/better-sqlite3-backend.js +801 -0
- package/dist/better-sqlite3-backend.js.map +1 -0
- package/dist/cli/save.js +0 -0
- package/dist/cli/setup.d.ts +6 -2
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +289 -42
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/viewer.js +25 -56
- package/dist/cli/viewer.js.map +1 -1
- package/dist/cli/web-viewer.d.ts +2 -1
- package/dist/cli/web-viewer.d.ts.map +1 -1
- package/dist/cli/web-viewer.js +791 -141
- package/dist/cli/web-viewer.js.map +1 -1
- package/dist/embeddings/embedding-cache.d.ts +131 -0
- package/dist/embeddings/embedding-cache.d.ts.map +1 -0
- package/dist/embeddings/embedding-cache.js +217 -0
- package/dist/embeddings/embedding-cache.js.map +1 -0
- package/dist/embeddings/index.d.ts +11 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +11 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/local-embeddings.d.ts +140 -0
- package/dist/embeddings/local-embeddings.d.ts.map +1 -0
- package/dist/embeddings/local-embeddings.js +293 -0
- package/dist/embeddings/local-embeddings.js.map +1 -0
- package/dist/hooks/context.d.ts +6 -1
- package/dist/hooks/context.d.ts.map +1 -1
- package/dist/hooks/context.js +12 -2
- package/dist/hooks/context.js.map +1 -1
- package/dist/hooks/observation.d.ts +6 -1
- package/dist/hooks/observation.d.ts.map +1 -1
- package/dist/hooks/observation.js +12 -2
- package/dist/hooks/observation.js.map +1 -1
- package/dist/hooks/service.d.ts +1 -6
- package/dist/hooks/service.d.ts.map +1 -1
- package/dist/hooks/service.js +33 -85
- package/dist/hooks/service.js.map +1 -1
- package/dist/hooks/session-init.d.ts +6 -1
- package/dist/hooks/session-init.d.ts.map +1 -1
- package/dist/hooks/session-init.js +12 -2
- package/dist/hooks/session-init.js.map +1 -1
- package/dist/hooks/summarize.d.ts +6 -1
- package/dist/hooks/summarize.d.ts.map +1 -1
- package/dist/hooks/summarize.js +12 -2
- package/dist/hooks/summarize.js.map +1 -1
- package/dist/index.d.ts +10 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -94
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +17 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/migration.js +3 -3
- package/dist/migration.js.map +1 -1
- package/dist/search/hybrid-search.d.ts +262 -0
- package/dist/search/hybrid-search.d.ts.map +1 -0
- package/dist/search/hybrid-search.js +688 -0
- package/dist/search/hybrid-search.js.map +1 -0
- package/dist/search/index.d.ts +13 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +13 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/token-economics.d.ts +161 -0
- package/dist/search/token-economics.d.ts.map +1 -0
- package/dist/search/token-economics.js +239 -0
- package/dist/search/token-economics.js.map +1 -0
- package/dist/types.d.ts +0 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/src/__tests__/better-sqlite3-backend.test.ts +1466 -0
- package/src/__tests__/cache-manager.test.ts +499 -0
- package/src/__tests__/embedding-integration.test.ts +481 -0
- package/src/__tests__/hnsw-index.test.ts +727 -0
- package/src/__tests__/index.test.ts +432 -0
- package/src/better-sqlite3-backend.ts +1000 -0
- package/src/cli/setup.ts +358 -47
- package/src/cli/viewer.ts +28 -63
- package/src/cli/web-viewer.ts +936 -182
- package/src/embeddings/__tests__/embedding-cache.test.ts +269 -0
- package/src/embeddings/__tests__/local-embeddings.test.ts +495 -0
- package/src/embeddings/embedding-cache.ts +318 -0
- package/src/embeddings/index.ts +20 -0
- package/src/embeddings/local-embeddings.ts +419 -0
- package/src/hooks/__tests__/handlers.test.ts +58 -17
- package/src/hooks/__tests__/integration.test.ts +77 -26
- package/src/hooks/context.ts +13 -2
- package/src/hooks/observation.ts +13 -2
- package/src/hooks/service.ts +39 -100
- package/src/hooks/session-init.ts +13 -2
- package/src/hooks/summarize.ts +13 -2
- package/src/index.ts +210 -116
- package/src/mcp/server.ts +20 -3
- package/src/search/__tests__/hybrid-search.test.ts +669 -0
- package/src/search/__tests__/token-economics.test.ts +276 -0
- package/src/search/hybrid-search.ts +968 -0
- package/src/search/index.ts +29 -0
- package/src/search/token-economics.ts +367 -0
- package/src/types.ts +0 -96
- package/src/__tests__/sqljs-backend.test.ts +0 -410
- package/src/migration.ts +0 -574
- package/src/sql.js.d.ts +0 -70
- package/src/sqljs-backend.ts +0 -789
|
@@ -0,0 +1,1466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for BetterSqlite3Backend with FTS5 Trigram Tokenizer for CJK Support
|
|
3
|
+
*
|
|
4
|
+
* These tests verify proper CJK (Japanese, Chinese, Korean) language support
|
|
5
|
+
* using the native SQLite trigram tokenizer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, beforeAll } from 'vitest';
|
|
9
|
+
import { BetterSqlite3Backend, createBetterSqlite3Backend, createJapaneseOptimizedBackend } from '../better-sqlite3-backend.js';
|
|
10
|
+
import type { MemoryEntry } from '../types.js';
|
|
11
|
+
|
|
12
|
+
// Skip tests if better-sqlite3 is not available
|
|
13
|
+
let betterSqlite3Available = false;
|
|
14
|
+
try {
|
|
15
|
+
await import('better-sqlite3');
|
|
16
|
+
betterSqlite3Available = true;
|
|
17
|
+
} catch {
|
|
18
|
+
console.log('[Test] better-sqlite3 not available, skipping native tests');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const describeCond = betterSqlite3Available ? describe : describe.skip;
|
|
22
|
+
|
|
23
|
+
describeCond('BetterSqlite3Backend', () => {
|
|
24
|
+
let backend: BetterSqlite3Backend;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
backend = createBetterSqlite3Backend({
|
|
28
|
+
databasePath: ':memory:',
|
|
29
|
+
ftsTokenizer: 'trigram',
|
|
30
|
+
verbose: false,
|
|
31
|
+
});
|
|
32
|
+
await backend.initialize();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(async () => {
|
|
36
|
+
await backend.shutdown();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('initialization', () => {
|
|
40
|
+
it('should initialize successfully', async () => {
|
|
41
|
+
expect(backend).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should have FTS5 available', () => {
|
|
45
|
+
expect(backend.isFtsAvailable()).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should use trigram tokenizer', () => {
|
|
49
|
+
expect(backend.getActiveTokenizer()).toBe('trigram');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should report CJK optimized', () => {
|
|
53
|
+
expect(backend.isCjkOptimized()).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should pass health check with CJK support', async () => {
|
|
57
|
+
const health = await backend.healthCheck();
|
|
58
|
+
expect(health.status).toBe('healthy');
|
|
59
|
+
|
|
60
|
+
// cache component is repurposed for CJK status
|
|
61
|
+
expect(health.components.cache.status).toBe('healthy');
|
|
62
|
+
expect(health.components.cache.message).toContain('Trigram');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('basic CRUD operations', () => {
|
|
67
|
+
it('should store and retrieve entries', async () => {
|
|
68
|
+
const entry: MemoryEntry = {
|
|
69
|
+
id: 'test-1',
|
|
70
|
+
key: 'test-key',
|
|
71
|
+
content: 'Test content',
|
|
72
|
+
type: 'semantic',
|
|
73
|
+
namespace: 'default',
|
|
74
|
+
tags: ['test'],
|
|
75
|
+
metadata: {},
|
|
76
|
+
accessLevel: 'project',
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
updatedAt: Date.now(),
|
|
79
|
+
version: 1,
|
|
80
|
+
references: [],
|
|
81
|
+
accessCount: 0,
|
|
82
|
+
lastAccessedAt: Date.now(),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await backend.store(entry);
|
|
86
|
+
const retrieved = await backend.get('test-1');
|
|
87
|
+
|
|
88
|
+
expect(retrieved).not.toBeNull();
|
|
89
|
+
expect(retrieved?.id).toBe('test-1');
|
|
90
|
+
expect(retrieved?.content).toBe('Test content');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should update entries', async () => {
|
|
94
|
+
const entry: MemoryEntry = {
|
|
95
|
+
id: 'test-update',
|
|
96
|
+
key: 'original-key',
|
|
97
|
+
content: 'Original content',
|
|
98
|
+
type: 'semantic',
|
|
99
|
+
namespace: 'default',
|
|
100
|
+
tags: [],
|
|
101
|
+
metadata: {},
|
|
102
|
+
accessLevel: 'project',
|
|
103
|
+
createdAt: Date.now(),
|
|
104
|
+
updatedAt: Date.now(),
|
|
105
|
+
version: 1,
|
|
106
|
+
references: [],
|
|
107
|
+
accessCount: 0,
|
|
108
|
+
lastAccessedAt: Date.now(),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await backend.store(entry);
|
|
112
|
+
const updated = await backend.update('test-update', { content: 'Updated content' });
|
|
113
|
+
|
|
114
|
+
expect(updated?.content).toBe('Updated content');
|
|
115
|
+
expect(updated?.version).toBe(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should delete entries', async () => {
|
|
119
|
+
const entry: MemoryEntry = {
|
|
120
|
+
id: 'test-delete',
|
|
121
|
+
key: 'delete-key',
|
|
122
|
+
content: 'Delete me',
|
|
123
|
+
type: 'semantic',
|
|
124
|
+
namespace: 'default',
|
|
125
|
+
tags: [],
|
|
126
|
+
metadata: {},
|
|
127
|
+
accessLevel: 'project',
|
|
128
|
+
createdAt: Date.now(),
|
|
129
|
+
updatedAt: Date.now(),
|
|
130
|
+
version: 1,
|
|
131
|
+
references: [],
|
|
132
|
+
accessCount: 0,
|
|
133
|
+
lastAccessedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
await backend.store(entry);
|
|
137
|
+
const deleted = await backend.delete('test-delete');
|
|
138
|
+
const retrieved = await backend.get('test-delete');
|
|
139
|
+
|
|
140
|
+
expect(deleted).toBe(true);
|
|
141
|
+
expect(retrieved).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('FTS5 with trigram tokenizer', () => {
|
|
146
|
+
beforeEach(async () => {
|
|
147
|
+
// Insert test entries
|
|
148
|
+
const entries: MemoryEntry[] = [
|
|
149
|
+
{
|
|
150
|
+
id: 'en-1',
|
|
151
|
+
key: 'english',
|
|
152
|
+
content: 'Authentication using JWT tokens with refresh mechanism',
|
|
153
|
+
type: 'semantic',
|
|
154
|
+
namespace: 'patterns',
|
|
155
|
+
tags: ['auth'],
|
|
156
|
+
metadata: {},
|
|
157
|
+
accessLevel: 'project',
|
|
158
|
+
createdAt: Date.now(),
|
|
159
|
+
updatedAt: Date.now(),
|
|
160
|
+
version: 1,
|
|
161
|
+
references: [],
|
|
162
|
+
accessCount: 0,
|
|
163
|
+
lastAccessedAt: Date.now(),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'en-2',
|
|
167
|
+
key: 'database',
|
|
168
|
+
content: 'PostgreSQL connection pooling for high performance',
|
|
169
|
+
type: 'semantic',
|
|
170
|
+
namespace: 'patterns',
|
|
171
|
+
tags: ['database'],
|
|
172
|
+
metadata: {},
|
|
173
|
+
accessLevel: 'project',
|
|
174
|
+
createdAt: Date.now(),
|
|
175
|
+
updatedAt: Date.now(),
|
|
176
|
+
version: 1,
|
|
177
|
+
references: [],
|
|
178
|
+
accessCount: 0,
|
|
179
|
+
lastAccessedAt: Date.now(),
|
|
180
|
+
},
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
await backend.bulkInsert(entries);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should find English entries by keyword', async () => {
|
|
187
|
+
const results = await backend.searchFts('authentication');
|
|
188
|
+
expect(results.length).toBeGreaterThan(0);
|
|
189
|
+
expect(results.some((r) => r.id === 'en-1')).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should find entries by partial match', async () => {
|
|
193
|
+
const results = await backend.searchFts('auth');
|
|
194
|
+
expect(results.length).toBeGreaterThan(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should filter by namespace', async () => {
|
|
198
|
+
const results = await backend.searchFts('authentication', { namespace: 'patterns' });
|
|
199
|
+
expect(results.every((r) => r.namespace === 'patterns')).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('CJK language support', () => {
|
|
204
|
+
describe('Japanese (日本語)', () => {
|
|
205
|
+
beforeEach(async () => {
|
|
206
|
+
const entries: MemoryEntry[] = [
|
|
207
|
+
{
|
|
208
|
+
id: 'jp-1',
|
|
209
|
+
key: 'japanese-1',
|
|
210
|
+
content: '日本語のテスト内容です。認証機能の実装について説明します。',
|
|
211
|
+
type: 'semantic',
|
|
212
|
+
namespace: 'japanese',
|
|
213
|
+
tags: ['日本語', 'テスト'],
|
|
214
|
+
metadata: {},
|
|
215
|
+
accessLevel: 'project',
|
|
216
|
+
createdAt: Date.now(),
|
|
217
|
+
updatedAt: Date.now(),
|
|
218
|
+
version: 1,
|
|
219
|
+
references: [],
|
|
220
|
+
accessCount: 0,
|
|
221
|
+
lastAccessedAt: Date.now(),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'jp-2',
|
|
225
|
+
key: 'japanese-2',
|
|
226
|
+
content: 'データベース接続プーリングの実装パターン',
|
|
227
|
+
type: 'semantic',
|
|
228
|
+
namespace: 'japanese',
|
|
229
|
+
tags: ['データベース'],
|
|
230
|
+
metadata: {},
|
|
231
|
+
accessLevel: 'project',
|
|
232
|
+
createdAt: Date.now(),
|
|
233
|
+
updatedAt: Date.now(),
|
|
234
|
+
version: 1,
|
|
235
|
+
references: [],
|
|
236
|
+
accessCount: 0,
|
|
237
|
+
lastAccessedAt: Date.now(),
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
await backend.bulkInsert(entries);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should find entries by Japanese text', async () => {
|
|
245
|
+
const results = await backend.searchFts('日本語');
|
|
246
|
+
expect(results.length).toBeGreaterThan(0);
|
|
247
|
+
expect(results.some((r) => r.id === 'jp-1')).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should find entries by Japanese partial text', async () => {
|
|
251
|
+
// Trigram tokenizer needs 3+ characters for reliable matching
|
|
252
|
+
const results = await backend.searchFts('認証機能');
|
|
253
|
+
expect(results.length).toBeGreaterThan(0);
|
|
254
|
+
expect(results.some((r) => r.id === 'jp-1')).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should find entries by katakana', async () => {
|
|
258
|
+
const results = await backend.searchFts('データベース');
|
|
259
|
+
expect(results.length).toBeGreaterThan(0);
|
|
260
|
+
expect(results.some((r) => r.id === 'jp-2')).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should find entries by hiragana', async () => {
|
|
264
|
+
// 'テスト内容' is a longer phrase that appears in the content
|
|
265
|
+
const results = await backend.searchFts('テスト内容');
|
|
266
|
+
expect(results.length).toBeGreaterThan(0);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('Chinese (中文)', () => {
|
|
271
|
+
beforeEach(async () => {
|
|
272
|
+
const entries: MemoryEntry[] = [
|
|
273
|
+
{
|
|
274
|
+
id: 'cn-1',
|
|
275
|
+
key: 'chinese-1',
|
|
276
|
+
content: '中文测试内容。这是关于用户认证的说明。',
|
|
277
|
+
type: 'semantic',
|
|
278
|
+
namespace: 'chinese',
|
|
279
|
+
tags: ['中文', '测试'],
|
|
280
|
+
metadata: {},
|
|
281
|
+
accessLevel: 'project',
|
|
282
|
+
createdAt: Date.now(),
|
|
283
|
+
updatedAt: Date.now(),
|
|
284
|
+
version: 1,
|
|
285
|
+
references: [],
|
|
286
|
+
accessCount: 0,
|
|
287
|
+
lastAccessedAt: Date.now(),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: 'cn-2',
|
|
291
|
+
key: 'chinese-2',
|
|
292
|
+
content: '数据库连接池配置说明',
|
|
293
|
+
type: 'semantic',
|
|
294
|
+
namespace: 'chinese',
|
|
295
|
+
tags: ['数据库'],
|
|
296
|
+
metadata: {},
|
|
297
|
+
accessLevel: 'project',
|
|
298
|
+
createdAt: Date.now(),
|
|
299
|
+
updatedAt: Date.now(),
|
|
300
|
+
version: 1,
|
|
301
|
+
references: [],
|
|
302
|
+
accessCount: 0,
|
|
303
|
+
lastAccessedAt: Date.now(),
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
await backend.bulkInsert(entries);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should find entries by Chinese text', async () => {
|
|
311
|
+
// Use 3+ character term for trigram tokenizer
|
|
312
|
+
const results = await backend.searchFts('中文测试');
|
|
313
|
+
expect(results.length).toBeGreaterThan(0);
|
|
314
|
+
expect(results.some((r) => r.id === 'cn-1')).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should find entries by Chinese partial text', async () => {
|
|
318
|
+
// Use longer phrase for reliable trigram matching
|
|
319
|
+
const results = await backend.searchFts('用户认证');
|
|
320
|
+
expect(results.length).toBeGreaterThan(0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should find entries by Chinese database term', async () => {
|
|
324
|
+
const results = await backend.searchFts('数据库');
|
|
325
|
+
expect(results.length).toBeGreaterThan(0);
|
|
326
|
+
expect(results.some((r) => r.id === 'cn-2')).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Korean (한국어)', () => {
|
|
331
|
+
beforeEach(async () => {
|
|
332
|
+
const entries: MemoryEntry[] = [
|
|
333
|
+
{
|
|
334
|
+
id: 'kr-1',
|
|
335
|
+
key: 'korean-1',
|
|
336
|
+
content: '한국어 테스트 내용입니다. 사용자 인증에 대한 설명입니다.',
|
|
337
|
+
type: 'semantic',
|
|
338
|
+
namespace: 'korean',
|
|
339
|
+
tags: ['한국어', '테스트'],
|
|
340
|
+
metadata: {},
|
|
341
|
+
accessLevel: 'project',
|
|
342
|
+
createdAt: Date.now(),
|
|
343
|
+
updatedAt: Date.now(),
|
|
344
|
+
version: 1,
|
|
345
|
+
references: [],
|
|
346
|
+
accessCount: 0,
|
|
347
|
+
lastAccessedAt: Date.now(),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
id: 'kr-2',
|
|
351
|
+
key: 'korean-2',
|
|
352
|
+
content: '데이터베이스 연결 풀 설정 방법',
|
|
353
|
+
type: 'semantic',
|
|
354
|
+
namespace: 'korean',
|
|
355
|
+
tags: ['데이터베이스'],
|
|
356
|
+
metadata: {},
|
|
357
|
+
accessLevel: 'project',
|
|
358
|
+
createdAt: Date.now(),
|
|
359
|
+
updatedAt: Date.now(),
|
|
360
|
+
version: 1,
|
|
361
|
+
references: [],
|
|
362
|
+
accessCount: 0,
|
|
363
|
+
lastAccessedAt: Date.now(),
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
await backend.bulkInsert(entries);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should find entries by Korean text', async () => {
|
|
371
|
+
const results = await backend.searchFts('한국어');
|
|
372
|
+
expect(results.length).toBeGreaterThan(0);
|
|
373
|
+
expect(results.some((r) => r.id === 'kr-1')).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should find entries by Korean partial text', async () => {
|
|
377
|
+
// Use longer phrase for reliable trigram matching
|
|
378
|
+
const results = await backend.searchFts('사용자 인증');
|
|
379
|
+
expect(results.length).toBeGreaterThan(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should find entries by Korean database term', async () => {
|
|
383
|
+
const results = await backend.searchFts('데이터베이스');
|
|
384
|
+
expect(results.length).toBeGreaterThan(0);
|
|
385
|
+
expect(results.some((r) => r.id === 'kr-2')).toBe(true);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('Mixed language support', () => {
|
|
390
|
+
beforeEach(async () => {
|
|
391
|
+
const entry: MemoryEntry = {
|
|
392
|
+
id: 'mixed-1',
|
|
393
|
+
key: 'mixed',
|
|
394
|
+
content: 'API設計パターン - Japanese API design patterns using REST and GraphQL',
|
|
395
|
+
type: 'semantic',
|
|
396
|
+
namespace: 'mixed',
|
|
397
|
+
tags: ['API', '設計'],
|
|
398
|
+
metadata: {},
|
|
399
|
+
accessLevel: 'project',
|
|
400
|
+
createdAt: Date.now(),
|
|
401
|
+
updatedAt: Date.now(),
|
|
402
|
+
version: 1,
|
|
403
|
+
references: [],
|
|
404
|
+
accessCount: 0,
|
|
405
|
+
lastAccessedAt: Date.now(),
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
await backend.store(entry);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should find by Japanese in mixed content', async () => {
|
|
412
|
+
const results = await backend.searchFts('設計パターン');
|
|
413
|
+
expect(results.length).toBeGreaterThan(0);
|
|
414
|
+
expect(results.some((r) => r.id === 'mixed-1')).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should find by English in mixed content', async () => {
|
|
418
|
+
const results = await backend.searchFts('GraphQL');
|
|
419
|
+
expect(results.length).toBeGreaterThan(0);
|
|
420
|
+
expect(results.some((r) => r.id === 'mixed-1')).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('should find by API term in mixed content', async () => {
|
|
424
|
+
const results = await backend.searchFts('API');
|
|
425
|
+
expect(results.length).toBeGreaterThan(0);
|
|
426
|
+
expect(results.some((r) => r.id === 'mixed-1')).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('edge cases', () => {
|
|
432
|
+
it('should handle empty query', async () => {
|
|
433
|
+
const results = await backend.searchFts('');
|
|
434
|
+
expect(results.length).toBe(0);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should handle whitespace-only query', async () => {
|
|
438
|
+
const results = await backend.searchFts(' ');
|
|
439
|
+
expect(results.length).toBe(0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('should handle special characters', async () => {
|
|
443
|
+
const results = await backend.searchFts('test*[query]');
|
|
444
|
+
expect(Array.isArray(results)).toBe(true);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('should handle very long content', async () => {
|
|
448
|
+
const longContent = '日本語テスト '.repeat(1000);
|
|
449
|
+
const entry: MemoryEntry = {
|
|
450
|
+
id: 'long-content',
|
|
451
|
+
key: 'long',
|
|
452
|
+
content: longContent,
|
|
453
|
+
type: 'semantic',
|
|
454
|
+
namespace: 'default',
|
|
455
|
+
tags: [],
|
|
456
|
+
metadata: {},
|
|
457
|
+
accessLevel: 'project',
|
|
458
|
+
createdAt: Date.now(),
|
|
459
|
+
updatedAt: Date.now(),
|
|
460
|
+
version: 1,
|
|
461
|
+
references: [],
|
|
462
|
+
accessCount: 0,
|
|
463
|
+
lastAccessedAt: Date.now(),
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
await backend.store(entry);
|
|
467
|
+
const results = await backend.searchFts('日本語');
|
|
468
|
+
expect(results.some((r) => r.id === 'long-content')).toBe(true);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('bulk operations', () => {
|
|
473
|
+
it('should handle bulk insert', async () => {
|
|
474
|
+
const entries: MemoryEntry[] = Array.from({ length: 100 }, (_, i) => ({
|
|
475
|
+
id: `bulk-${i}`,
|
|
476
|
+
key: `key-${i}`,
|
|
477
|
+
content: `Bulk content ${i} with 日本語 and 中文`,
|
|
478
|
+
type: 'semantic' as const,
|
|
479
|
+
namespace: 'bulk',
|
|
480
|
+
tags: [],
|
|
481
|
+
metadata: {},
|
|
482
|
+
accessLevel: 'project' as const,
|
|
483
|
+
createdAt: Date.now(),
|
|
484
|
+
updatedAt: Date.now(),
|
|
485
|
+
version: 1,
|
|
486
|
+
references: [],
|
|
487
|
+
accessCount: 0,
|
|
488
|
+
lastAccessedAt: Date.now(),
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
await backend.bulkInsert(entries);
|
|
492
|
+
const count = await backend.count('bulk');
|
|
493
|
+
expect(count).toBe(100);
|
|
494
|
+
|
|
495
|
+
// FTS should work on bulk inserted entries
|
|
496
|
+
const results = await backend.searchFts('日本語', { namespace: 'bulk' });
|
|
497
|
+
expect(results.length).toBeGreaterThan(0);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should handle bulk delete', async () => {
|
|
501
|
+
const entries: MemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({
|
|
502
|
+
id: `delete-${i}`,
|
|
503
|
+
key: `key-${i}`,
|
|
504
|
+
content: `Delete content ${i}`,
|
|
505
|
+
type: 'semantic' as const,
|
|
506
|
+
namespace: 'delete',
|
|
507
|
+
tags: [],
|
|
508
|
+
metadata: {},
|
|
509
|
+
accessLevel: 'project' as const,
|
|
510
|
+
createdAt: Date.now(),
|
|
511
|
+
updatedAt: Date.now(),
|
|
512
|
+
version: 1,
|
|
513
|
+
references: [],
|
|
514
|
+
accessCount: 0,
|
|
515
|
+
lastAccessedAt: Date.now(),
|
|
516
|
+
}));
|
|
517
|
+
|
|
518
|
+
await backend.bulkInsert(entries);
|
|
519
|
+
const deleted = await backend.bulkDelete(entries.slice(0, 5).map((e) => e.id));
|
|
520
|
+
const remaining = await backend.count('delete');
|
|
521
|
+
|
|
522
|
+
expect(deleted).toBe(5);
|
|
523
|
+
expect(remaining).toBe(5);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('statistics', () => {
|
|
528
|
+
it('should return correct stats', async () => {
|
|
529
|
+
const entries: MemoryEntry[] = Array.from({ length: 10 }, (_, i) => ({
|
|
530
|
+
id: `stats-${i}`,
|
|
531
|
+
key: `key-${i}`,
|
|
532
|
+
content: `Stats content ${i}`,
|
|
533
|
+
type: 'semantic' as const,
|
|
534
|
+
namespace: i < 5 ? 'ns1' : 'ns2',
|
|
535
|
+
tags: [],
|
|
536
|
+
metadata: {},
|
|
537
|
+
accessLevel: 'project' as const,
|
|
538
|
+
createdAt: Date.now(),
|
|
539
|
+
updatedAt: Date.now(),
|
|
540
|
+
version: 1,
|
|
541
|
+
references: [],
|
|
542
|
+
accessCount: 0,
|
|
543
|
+
lastAccessedAt: Date.now(),
|
|
544
|
+
}));
|
|
545
|
+
|
|
546
|
+
await backend.bulkInsert(entries);
|
|
547
|
+
const stats = await backend.getStats();
|
|
548
|
+
|
|
549
|
+
expect(stats.totalEntries).toBe(10);
|
|
550
|
+
expect(stats.entriesByNamespace.ns1).toBe(5);
|
|
551
|
+
expect(stats.entriesByNamespace.ns2).toBe(5);
|
|
552
|
+
expect(stats.memoryUsage).toBeGreaterThan(0);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
describe('createBetterSqlite3Backend factory', () => {
|
|
558
|
+
const describeCond = betterSqlite3Available ? describe : describe.skip;
|
|
559
|
+
|
|
560
|
+
describeCond('factory function', () => {
|
|
561
|
+
it('should create backend with default trigram tokenizer', async () => {
|
|
562
|
+
const backend = createBetterSqlite3Backend({
|
|
563
|
+
databasePath: ':memory:',
|
|
564
|
+
});
|
|
565
|
+
await backend.initialize();
|
|
566
|
+
|
|
567
|
+
expect(backend.getActiveTokenizer()).toBe('trigram');
|
|
568
|
+
expect(backend.isCjkOptimized()).toBe(true);
|
|
569
|
+
|
|
570
|
+
await backend.shutdown();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it('should allow custom tokenizer', async () => {
|
|
574
|
+
const backend = createBetterSqlite3Backend({
|
|
575
|
+
databasePath: ':memory:',
|
|
576
|
+
ftsTokenizer: 'unicode61',
|
|
577
|
+
});
|
|
578
|
+
await backend.initialize();
|
|
579
|
+
|
|
580
|
+
expect(backend.getActiveTokenizer()).toBe('unicode61');
|
|
581
|
+
expect(backend.isCjkOptimized()).toBe(false);
|
|
582
|
+
|
|
583
|
+
await backend.shutdown();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should rebuild FTS index', async () => {
|
|
587
|
+
const backend = createBetterSqlite3Backend({
|
|
588
|
+
databasePath: ':memory:',
|
|
589
|
+
});
|
|
590
|
+
await backend.initialize();
|
|
591
|
+
|
|
592
|
+
// Store an entry
|
|
593
|
+
await backend.store({
|
|
594
|
+
id: 'rebuild-test',
|
|
595
|
+
key: 'rebuild-key',
|
|
596
|
+
content: 'Content for FTS rebuild test',
|
|
597
|
+
type: 'semantic',
|
|
598
|
+
namespace: 'test',
|
|
599
|
+
tags: ['rebuild'],
|
|
600
|
+
metadata: {},
|
|
601
|
+
accessLevel: 'project',
|
|
602
|
+
createdAt: Date.now(),
|
|
603
|
+
updatedAt: Date.now(),
|
|
604
|
+
version: 1,
|
|
605
|
+
references: [],
|
|
606
|
+
accessCount: 0,
|
|
607
|
+
lastAccessedAt: Date.now(),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// Rebuild should not throw
|
|
611
|
+
await expect(backend.rebuildFtsIndex()).resolves.not.toThrow();
|
|
612
|
+
|
|
613
|
+
// Search should still work after rebuild
|
|
614
|
+
const results = await backend.query({ type: 'keyword', content: 'rebuild' });
|
|
615
|
+
expect(results.length).toBeGreaterThan(0);
|
|
616
|
+
|
|
617
|
+
await backend.shutdown();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should handle entries with embeddings', async () => {
|
|
621
|
+
const backend = createBetterSqlite3Backend({
|
|
622
|
+
databasePath: ':memory:',
|
|
623
|
+
});
|
|
624
|
+
await backend.initialize();
|
|
625
|
+
|
|
626
|
+
// Create an entry with embedding
|
|
627
|
+
const embedding = new Float32Array(384);
|
|
628
|
+
for (let i = 0; i < 384; i++) {
|
|
629
|
+
embedding[i] = i / 384;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
await backend.store({
|
|
633
|
+
id: 'emb-test',
|
|
634
|
+
key: 'embedding-key',
|
|
635
|
+
content: 'Content with vector embedding',
|
|
636
|
+
type: 'semantic',
|
|
637
|
+
namespace: 'embeddings',
|
|
638
|
+
tags: ['vector'],
|
|
639
|
+
metadata: { hasEmbedding: true },
|
|
640
|
+
embedding,
|
|
641
|
+
accessLevel: 'project',
|
|
642
|
+
createdAt: Date.now(),
|
|
643
|
+
updatedAt: Date.now(),
|
|
644
|
+
version: 1,
|
|
645
|
+
references: [],
|
|
646
|
+
accessCount: 0,
|
|
647
|
+
lastAccessedAt: Date.now(),
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Retrieve and verify embedding is preserved
|
|
651
|
+
const entry = await backend.get('emb-test');
|
|
652
|
+
expect(entry).toBeDefined();
|
|
653
|
+
expect(entry?.embedding).toBeDefined();
|
|
654
|
+
expect(entry?.embedding?.length).toBe(384);
|
|
655
|
+
expect(entry?.embedding?.[0]).toBeCloseTo(0, 5);
|
|
656
|
+
expect(entry?.embedding?.[100]).toBeCloseTo(100 / 384, 5);
|
|
657
|
+
expect(entry?.embedding?.[383]).toBeCloseTo(383 / 384, 5);
|
|
658
|
+
|
|
659
|
+
await backend.shutdown();
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
it('should create Japanese optimized backend', () => {
|
|
663
|
+
// Note: This test verifies configuration, not actual lindera loading
|
|
664
|
+
// since lindera extension needs to be built separately
|
|
665
|
+
const backend = createJapaneseOptimizedBackend({
|
|
666
|
+
databasePath: ':memory:',
|
|
667
|
+
linderaPath: '/path/to/liblindera_sqlite.dylib',
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Backend is created with the configuration
|
|
671
|
+
expect(backend).toBeDefined();
|
|
672
|
+
// Note: initialization would fail without the actual extension file
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
describeCond('BetterSqlite3Backend advanced', () => {
|
|
678
|
+
let backend: BetterSqlite3Backend;
|
|
679
|
+
|
|
680
|
+
beforeEach(async () => {
|
|
681
|
+
backend = createBetterSqlite3Backend({
|
|
682
|
+
databasePath: ':memory:',
|
|
683
|
+
ftsTokenizer: 'trigram',
|
|
684
|
+
verbose: false,
|
|
685
|
+
});
|
|
686
|
+
await backend.initialize();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
afterEach(async () => {
|
|
690
|
+
await backend.shutdown();
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
describe('query filters', () => {
|
|
694
|
+
beforeEach(async () => {
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
const entries: MemoryEntry[] = [
|
|
697
|
+
{
|
|
698
|
+
id: 'old-entry',
|
|
699
|
+
key: 'old',
|
|
700
|
+
content: 'Old content',
|
|
701
|
+
type: 'episodic',
|
|
702
|
+
namespace: 'time-test',
|
|
703
|
+
tags: ['old'],
|
|
704
|
+
metadata: {},
|
|
705
|
+
accessLevel: 'project',
|
|
706
|
+
createdAt: now - 100000,
|
|
707
|
+
updatedAt: now - 100000,
|
|
708
|
+
version: 1,
|
|
709
|
+
references: [],
|
|
710
|
+
accessCount: 0,
|
|
711
|
+
lastAccessedAt: now - 100000,
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
id: 'new-entry',
|
|
715
|
+
key: 'new',
|
|
716
|
+
content: 'New content',
|
|
717
|
+
type: 'semantic',
|
|
718
|
+
namespace: 'time-test',
|
|
719
|
+
tags: ['new'],
|
|
720
|
+
metadata: {},
|
|
721
|
+
accessLevel: 'project',
|
|
722
|
+
createdAt: now,
|
|
723
|
+
updatedAt: now,
|
|
724
|
+
version: 1,
|
|
725
|
+
references: [],
|
|
726
|
+
accessCount: 0,
|
|
727
|
+
lastAccessedAt: now,
|
|
728
|
+
},
|
|
729
|
+
];
|
|
730
|
+
await backend.bulkInsert(entries);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('should filter by createdBefore', async () => {
|
|
734
|
+
const results = await backend.query({
|
|
735
|
+
type: 'hybrid',
|
|
736
|
+
namespace: 'time-test',
|
|
737
|
+
createdBefore: Date.now() - 50000,
|
|
738
|
+
limit: 10,
|
|
739
|
+
});
|
|
740
|
+
expect(results.length).toBe(1);
|
|
741
|
+
expect(results[0].id).toBe('old-entry');
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it('should filter by createdAfter', async () => {
|
|
745
|
+
const results = await backend.query({
|
|
746
|
+
type: 'hybrid',
|
|
747
|
+
namespace: 'time-test',
|
|
748
|
+
createdAfter: Date.now() - 50000,
|
|
749
|
+
limit: 10,
|
|
750
|
+
});
|
|
751
|
+
expect(results.length).toBe(1);
|
|
752
|
+
expect(results[0].id).toBe('new-entry');
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('should filter by memoryType', async () => {
|
|
756
|
+
const results = await backend.query({
|
|
757
|
+
type: 'hybrid',
|
|
758
|
+
namespace: 'time-test',
|
|
759
|
+
memoryType: 'episodic',
|
|
760
|
+
limit: 10,
|
|
761
|
+
});
|
|
762
|
+
expect(results.length).toBe(1);
|
|
763
|
+
expect(results[0].id).toBe('old-entry');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it('should filter by tags', async () => {
|
|
767
|
+
const results = await backend.query({
|
|
768
|
+
type: 'hybrid',
|
|
769
|
+
namespace: 'time-test',
|
|
770
|
+
tags: ['old'],
|
|
771
|
+
limit: 10,
|
|
772
|
+
});
|
|
773
|
+
expect(results.length).toBe(1);
|
|
774
|
+
expect(results[0].id).toBe('old-entry');
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('should filter by multiple tags', async () => {
|
|
778
|
+
await backend.store({
|
|
779
|
+
id: 'multi-tag',
|
|
780
|
+
key: 'multi',
|
|
781
|
+
content: 'Multi tag content',
|
|
782
|
+
type: 'semantic',
|
|
783
|
+
namespace: 'time-test',
|
|
784
|
+
tags: ['old', 'new', 'special'],
|
|
785
|
+
metadata: {},
|
|
786
|
+
accessLevel: 'project',
|
|
787
|
+
createdAt: Date.now(),
|
|
788
|
+
updatedAt: Date.now(),
|
|
789
|
+
version: 1,
|
|
790
|
+
references: [],
|
|
791
|
+
accessCount: 0,
|
|
792
|
+
lastAccessedAt: Date.now(),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
const results = await backend.query({
|
|
796
|
+
type: 'hybrid',
|
|
797
|
+
namespace: 'time-test',
|
|
798
|
+
tags: ['special'],
|
|
799
|
+
limit: 10,
|
|
800
|
+
});
|
|
801
|
+
expect(results.length).toBe(1);
|
|
802
|
+
expect(results[0].id).toBe('multi-tag');
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
describe('getByKey', () => {
|
|
807
|
+
it('should retrieve entry by namespace and key', async () => {
|
|
808
|
+
await backend.store({
|
|
809
|
+
id: 'key-test-1',
|
|
810
|
+
key: 'unique-key',
|
|
811
|
+
content: 'Content by key',
|
|
812
|
+
type: 'semantic',
|
|
813
|
+
namespace: 'key-ns',
|
|
814
|
+
tags: [],
|
|
815
|
+
metadata: {},
|
|
816
|
+
accessLevel: 'project',
|
|
817
|
+
createdAt: Date.now(),
|
|
818
|
+
updatedAt: Date.now(),
|
|
819
|
+
version: 1,
|
|
820
|
+
references: [],
|
|
821
|
+
accessCount: 0,
|
|
822
|
+
lastAccessedAt: Date.now(),
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
const result = await backend.getByKey('key-ns', 'unique-key');
|
|
826
|
+
expect(result).not.toBeNull();
|
|
827
|
+
expect(result?.id).toBe('key-test-1');
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('should return null for non-existent key', async () => {
|
|
831
|
+
const result = await backend.getByKey('non-existent-ns', 'non-existent-key');
|
|
832
|
+
expect(result).toBeNull();
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should increment access count on getByKey', async () => {
|
|
836
|
+
await backend.store({
|
|
837
|
+
id: 'access-test',
|
|
838
|
+
key: 'access-key',
|
|
839
|
+
content: 'Access content',
|
|
840
|
+
type: 'semantic',
|
|
841
|
+
namespace: 'access-ns',
|
|
842
|
+
tags: [],
|
|
843
|
+
metadata: {},
|
|
844
|
+
accessLevel: 'project',
|
|
845
|
+
createdAt: Date.now(),
|
|
846
|
+
updatedAt: Date.now(),
|
|
847
|
+
version: 1,
|
|
848
|
+
references: [],
|
|
849
|
+
accessCount: 0,
|
|
850
|
+
lastAccessedAt: Date.now(),
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await backend.getByKey('access-ns', 'access-key');
|
|
854
|
+
const result = await backend.getByKey('access-ns', 'access-key');
|
|
855
|
+
expect(result?.accessCount).toBeGreaterThanOrEqual(1);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
describe('semantic search', () => {
|
|
860
|
+
it('should perform vector search with embeddings', async () => {
|
|
861
|
+
// Create distinct vectors - embedding1 is similar to query, embedding2 is different
|
|
862
|
+
const embedding1 = new Float32Array([1, 0, 0, 0, 0, 0, 0, 0]);
|
|
863
|
+
const embedding2 = new Float32Array([0, 1, 0, 0, 0, 0, 0, 0]);
|
|
864
|
+
|
|
865
|
+
await backend.store({
|
|
866
|
+
id: 'vec-1',
|
|
867
|
+
key: 'vector-1',
|
|
868
|
+
content: 'Vector content 1',
|
|
869
|
+
type: 'semantic',
|
|
870
|
+
namespace: 'vectors',
|
|
871
|
+
tags: [],
|
|
872
|
+
metadata: {},
|
|
873
|
+
embedding: embedding1,
|
|
874
|
+
accessLevel: 'project',
|
|
875
|
+
createdAt: Date.now(),
|
|
876
|
+
updatedAt: Date.now(),
|
|
877
|
+
version: 1,
|
|
878
|
+
references: [],
|
|
879
|
+
accessCount: 0,
|
|
880
|
+
lastAccessedAt: Date.now(),
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
await backend.store({
|
|
884
|
+
id: 'vec-2',
|
|
885
|
+
key: 'vector-2',
|
|
886
|
+
content: 'Vector content 2',
|
|
887
|
+
type: 'semantic',
|
|
888
|
+
namespace: 'vectors',
|
|
889
|
+
tags: [],
|
|
890
|
+
metadata: {},
|
|
891
|
+
embedding: embedding2,
|
|
892
|
+
accessLevel: 'project',
|
|
893
|
+
createdAt: Date.now(),
|
|
894
|
+
updatedAt: Date.now(),
|
|
895
|
+
version: 1,
|
|
896
|
+
references: [],
|
|
897
|
+
accessCount: 0,
|
|
898
|
+
lastAccessedAt: Date.now(),
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Query similar to embedding1
|
|
902
|
+
const queryEmbedding = new Float32Array([1, 0, 0, 0, 0, 0, 0, 0]);
|
|
903
|
+
const results = await backend.search(queryEmbedding, { k: 2 });
|
|
904
|
+
|
|
905
|
+
expect(results.length).toBe(2);
|
|
906
|
+
// First result should be vec-1 (identical to query)
|
|
907
|
+
expect(results[0].entry.id).toBe('vec-1');
|
|
908
|
+
expect(results[0].score).toBeCloseTo(1, 5); // Identical vectors
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
it('should apply namespace filter in vector search', async () => {
|
|
912
|
+
const embedding = new Float32Array(8).fill(0.5);
|
|
913
|
+
|
|
914
|
+
await backend.store({
|
|
915
|
+
id: 'vec-ns1',
|
|
916
|
+
key: 'vector-ns1',
|
|
917
|
+
content: 'Content 1',
|
|
918
|
+
type: 'semantic',
|
|
919
|
+
namespace: 'ns1',
|
|
920
|
+
tags: [],
|
|
921
|
+
metadata: {},
|
|
922
|
+
embedding,
|
|
923
|
+
accessLevel: 'project',
|
|
924
|
+
createdAt: Date.now(),
|
|
925
|
+
updatedAt: Date.now(),
|
|
926
|
+
version: 1,
|
|
927
|
+
references: [],
|
|
928
|
+
accessCount: 0,
|
|
929
|
+
lastAccessedAt: Date.now(),
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
await backend.store({
|
|
933
|
+
id: 'vec-ns2',
|
|
934
|
+
key: 'vector-ns2',
|
|
935
|
+
content: 'Content 2',
|
|
936
|
+
type: 'semantic',
|
|
937
|
+
namespace: 'ns2',
|
|
938
|
+
tags: [],
|
|
939
|
+
metadata: {},
|
|
940
|
+
embedding,
|
|
941
|
+
accessLevel: 'project',
|
|
942
|
+
createdAt: Date.now(),
|
|
943
|
+
updatedAt: Date.now(),
|
|
944
|
+
version: 1,
|
|
945
|
+
references: [],
|
|
946
|
+
accessCount: 0,
|
|
947
|
+
lastAccessedAt: Date.now(),
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const results = await backend.search(embedding, {
|
|
951
|
+
k: 10,
|
|
952
|
+
filters: { namespace: 'ns1' },
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(results.length).toBe(1);
|
|
956
|
+
expect(results[0].entry.namespace).toBe('ns1');
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it('should apply memoryType filter in vector search', async () => {
|
|
960
|
+
const embedding = new Float32Array(8).fill(0.5);
|
|
961
|
+
|
|
962
|
+
await backend.store({
|
|
963
|
+
id: 'vec-type1',
|
|
964
|
+
key: 'vector-type1',
|
|
965
|
+
content: 'Content 1',
|
|
966
|
+
type: 'episodic',
|
|
967
|
+
namespace: 'types',
|
|
968
|
+
tags: [],
|
|
969
|
+
metadata: {},
|
|
970
|
+
embedding,
|
|
971
|
+
accessLevel: 'project',
|
|
972
|
+
createdAt: Date.now(),
|
|
973
|
+
updatedAt: Date.now(),
|
|
974
|
+
version: 1,
|
|
975
|
+
references: [],
|
|
976
|
+
accessCount: 0,
|
|
977
|
+
lastAccessedAt: Date.now(),
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
await backend.store({
|
|
981
|
+
id: 'vec-type2',
|
|
982
|
+
key: 'vector-type2',
|
|
983
|
+
content: 'Content 2',
|
|
984
|
+
type: 'semantic',
|
|
985
|
+
namespace: 'types',
|
|
986
|
+
tags: [],
|
|
987
|
+
metadata: {},
|
|
988
|
+
embedding,
|
|
989
|
+
accessLevel: 'project',
|
|
990
|
+
createdAt: Date.now(),
|
|
991
|
+
updatedAt: Date.now(),
|
|
992
|
+
version: 1,
|
|
993
|
+
references: [],
|
|
994
|
+
accessCount: 0,
|
|
995
|
+
lastAccessedAt: Date.now(),
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const results = await backend.search(embedding, {
|
|
999
|
+
k: 10,
|
|
1000
|
+
filters: { memoryType: 'episodic' },
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
expect(results.length).toBe(1);
|
|
1004
|
+
expect(results[0].entry.type).toBe('episodic');
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
it('should apply threshold filter in vector search', async () => {
|
|
1008
|
+
const embedding1 = new Float32Array(8).fill(1);
|
|
1009
|
+
const embedding2 = new Float32Array(8).fill(-1);
|
|
1010
|
+
|
|
1011
|
+
await backend.store({
|
|
1012
|
+
id: 'vec-sim',
|
|
1013
|
+
key: 'similar',
|
|
1014
|
+
content: 'Similar content',
|
|
1015
|
+
type: 'semantic',
|
|
1016
|
+
namespace: 'threshold',
|
|
1017
|
+
tags: [],
|
|
1018
|
+
metadata: {},
|
|
1019
|
+
embedding: embedding1,
|
|
1020
|
+
accessLevel: 'project',
|
|
1021
|
+
createdAt: Date.now(),
|
|
1022
|
+
updatedAt: Date.now(),
|
|
1023
|
+
version: 1,
|
|
1024
|
+
references: [],
|
|
1025
|
+
accessCount: 0,
|
|
1026
|
+
lastAccessedAt: Date.now(),
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
await backend.store({
|
|
1030
|
+
id: 'vec-diff',
|
|
1031
|
+
key: 'different',
|
|
1032
|
+
content: 'Different content',
|
|
1033
|
+
type: 'semantic',
|
|
1034
|
+
namespace: 'threshold',
|
|
1035
|
+
tags: [],
|
|
1036
|
+
metadata: {},
|
|
1037
|
+
embedding: embedding2,
|
|
1038
|
+
accessLevel: 'project',
|
|
1039
|
+
createdAt: Date.now(),
|
|
1040
|
+
updatedAt: Date.now(),
|
|
1041
|
+
version: 1,
|
|
1042
|
+
references: [],
|
|
1043
|
+
accessCount: 0,
|
|
1044
|
+
lastAccessedAt: Date.now(),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
const queryEmbedding = new Float32Array(8).fill(1);
|
|
1048
|
+
const results = await backend.search(queryEmbedding, {
|
|
1049
|
+
k: 10,
|
|
1050
|
+
threshold: 0.9, // High threshold should filter out dissimilar
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
expect(results.length).toBe(1);
|
|
1054
|
+
expect(results[0].entry.id).toBe('vec-sim');
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should handle vector length mismatch', async () => {
|
|
1058
|
+
const embedding1 = new Float32Array(8).fill(0.5);
|
|
1059
|
+
const embedding2 = new Float32Array(16).fill(0.5); // Different size
|
|
1060
|
+
|
|
1061
|
+
await backend.store({
|
|
1062
|
+
id: 'vec-size1',
|
|
1063
|
+
key: 'size1',
|
|
1064
|
+
content: 'Content 1',
|
|
1065
|
+
type: 'semantic',
|
|
1066
|
+
namespace: 'sizes',
|
|
1067
|
+
tags: [],
|
|
1068
|
+
metadata: {},
|
|
1069
|
+
embedding: embedding1,
|
|
1070
|
+
accessLevel: 'project',
|
|
1071
|
+
createdAt: Date.now(),
|
|
1072
|
+
updatedAt: Date.now(),
|
|
1073
|
+
version: 1,
|
|
1074
|
+
references: [],
|
|
1075
|
+
accessCount: 0,
|
|
1076
|
+
lastAccessedAt: Date.now(),
|
|
1077
|
+
});
|
|
1078
|
+
|
|
1079
|
+
await backend.store({
|
|
1080
|
+
id: 'vec-size2',
|
|
1081
|
+
key: 'size2',
|
|
1082
|
+
content: 'Content 2',
|
|
1083
|
+
type: 'semantic',
|
|
1084
|
+
namespace: 'sizes',
|
|
1085
|
+
tags: [],
|
|
1086
|
+
metadata: {},
|
|
1087
|
+
embedding: embedding2,
|
|
1088
|
+
accessLevel: 'project',
|
|
1089
|
+
createdAt: Date.now(),
|
|
1090
|
+
updatedAt: Date.now(),
|
|
1091
|
+
version: 1,
|
|
1092
|
+
references: [],
|
|
1093
|
+
accessCount: 0,
|
|
1094
|
+
lastAccessedAt: Date.now(),
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
const queryEmbedding = new Float32Array(8).fill(0.5);
|
|
1098
|
+
const results = await backend.search(queryEmbedding, { k: 10 });
|
|
1099
|
+
|
|
1100
|
+
// Should still return results, mismatched sizes get similarity 0
|
|
1101
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
1102
|
+
});
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
describe('namespace operations', () => {
|
|
1106
|
+
it('should list all namespaces', async () => {
|
|
1107
|
+
await backend.store({
|
|
1108
|
+
id: 'ns-test-1',
|
|
1109
|
+
key: 'key1',
|
|
1110
|
+
content: 'Content 1',
|
|
1111
|
+
type: 'semantic',
|
|
1112
|
+
namespace: 'namespace-a',
|
|
1113
|
+
tags: [],
|
|
1114
|
+
metadata: {},
|
|
1115
|
+
accessLevel: 'project',
|
|
1116
|
+
createdAt: Date.now(),
|
|
1117
|
+
updatedAt: Date.now(),
|
|
1118
|
+
version: 1,
|
|
1119
|
+
references: [],
|
|
1120
|
+
accessCount: 0,
|
|
1121
|
+
lastAccessedAt: Date.now(),
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
await backend.store({
|
|
1125
|
+
id: 'ns-test-2',
|
|
1126
|
+
key: 'key2',
|
|
1127
|
+
content: 'Content 2',
|
|
1128
|
+
type: 'semantic',
|
|
1129
|
+
namespace: 'namespace-b',
|
|
1130
|
+
tags: [],
|
|
1131
|
+
metadata: {},
|
|
1132
|
+
accessLevel: 'project',
|
|
1133
|
+
createdAt: Date.now(),
|
|
1134
|
+
updatedAt: Date.now(),
|
|
1135
|
+
version: 1,
|
|
1136
|
+
references: [],
|
|
1137
|
+
accessCount: 0,
|
|
1138
|
+
lastAccessedAt: Date.now(),
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const namespaces = await backend.listNamespaces();
|
|
1142
|
+
expect(namespaces).toContain('namespace-a');
|
|
1143
|
+
expect(namespaces).toContain('namespace-b');
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
it('should clear namespace', async () => {
|
|
1147
|
+
await backend.store({
|
|
1148
|
+
id: 'clear-1',
|
|
1149
|
+
key: 'key1',
|
|
1150
|
+
content: 'Content 1',
|
|
1151
|
+
type: 'semantic',
|
|
1152
|
+
namespace: 'to-clear',
|
|
1153
|
+
tags: [],
|
|
1154
|
+
metadata: {},
|
|
1155
|
+
accessLevel: 'project',
|
|
1156
|
+
createdAt: Date.now(),
|
|
1157
|
+
updatedAt: Date.now(),
|
|
1158
|
+
version: 1,
|
|
1159
|
+
references: [],
|
|
1160
|
+
accessCount: 0,
|
|
1161
|
+
lastAccessedAt: Date.now(),
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await backend.store({
|
|
1165
|
+
id: 'clear-2',
|
|
1166
|
+
key: 'key2',
|
|
1167
|
+
content: 'Content 2',
|
|
1168
|
+
type: 'semantic',
|
|
1169
|
+
namespace: 'to-keep',
|
|
1170
|
+
tags: [],
|
|
1171
|
+
metadata: {},
|
|
1172
|
+
accessLevel: 'project',
|
|
1173
|
+
createdAt: Date.now(),
|
|
1174
|
+
updatedAt: Date.now(),
|
|
1175
|
+
version: 1,
|
|
1176
|
+
references: [],
|
|
1177
|
+
accessCount: 0,
|
|
1178
|
+
lastAccessedAt: Date.now(),
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
const cleared = await backend.clearNamespace('to-clear');
|
|
1182
|
+
expect(cleared).toBe(1);
|
|
1183
|
+
|
|
1184
|
+
const remainingCleared = await backend.count('to-clear');
|
|
1185
|
+
const remainingKept = await backend.count('to-keep');
|
|
1186
|
+
expect(remainingCleared).toBe(0);
|
|
1187
|
+
expect(remainingKept).toBe(1);
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
describe('update operations', () => {
|
|
1192
|
+
it('should return null when updating non-existent entry', async () => {
|
|
1193
|
+
const result = await backend.update('non-existent-id', { content: 'New content' });
|
|
1194
|
+
expect(result).toBeNull();
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it('should update multiple fields', async () => {
|
|
1198
|
+
await backend.store({
|
|
1199
|
+
id: 'update-test',
|
|
1200
|
+
key: 'update-key',
|
|
1201
|
+
content: 'Original content',
|
|
1202
|
+
type: 'semantic',
|
|
1203
|
+
namespace: 'update-ns',
|
|
1204
|
+
tags: ['original'],
|
|
1205
|
+
metadata: { original: true },
|
|
1206
|
+
accessLevel: 'project',
|
|
1207
|
+
createdAt: Date.now(),
|
|
1208
|
+
updatedAt: Date.now(),
|
|
1209
|
+
version: 1,
|
|
1210
|
+
references: [],
|
|
1211
|
+
accessCount: 0,
|
|
1212
|
+
lastAccessedAt: Date.now(),
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
const updated = await backend.update('update-test', {
|
|
1216
|
+
content: 'Updated content',
|
|
1217
|
+
tags: ['updated'],
|
|
1218
|
+
metadata: { updated: true },
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
expect(updated?.content).toBe('Updated content');
|
|
1222
|
+
expect(updated?.tags).toContain('updated');
|
|
1223
|
+
expect(updated?.metadata.updated).toBe(true);
|
|
1224
|
+
expect(updated?.version).toBe(2);
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
describe('health check scenarios', () => {
|
|
1229
|
+
it('should report degraded status when FTS is not available', async () => {
|
|
1230
|
+
// Create backend with unicode tokenizer (not CJK optimized)
|
|
1231
|
+
const nonCjkBackend = createBetterSqlite3Backend({
|
|
1232
|
+
databasePath: ':memory:',
|
|
1233
|
+
ftsTokenizer: 'unicode61',
|
|
1234
|
+
});
|
|
1235
|
+
await nonCjkBackend.initialize();
|
|
1236
|
+
|
|
1237
|
+
const health = await nonCjkBackend.healthCheck();
|
|
1238
|
+
expect(health.components.cache.status).toBe('degraded');
|
|
1239
|
+
expect(health.recommendations.length).toBeGreaterThan(0);
|
|
1240
|
+
|
|
1241
|
+
await nonCjkBackend.shutdown();
|
|
1242
|
+
});
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
describe('getDatabase', () => {
|
|
1246
|
+
it('should return the underlying database', () => {
|
|
1247
|
+
const db = backend.getDatabase();
|
|
1248
|
+
expect(db).not.toBeNull();
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
describe('events', () => {
|
|
1253
|
+
it('should emit entry:stored event', async () => {
|
|
1254
|
+
const events: unknown[] = [];
|
|
1255
|
+
backend.on('entry:stored', (data) => events.push(data));
|
|
1256
|
+
|
|
1257
|
+
await backend.store({
|
|
1258
|
+
id: 'event-test',
|
|
1259
|
+
key: 'event-key',
|
|
1260
|
+
content: 'Event content',
|
|
1261
|
+
type: 'semantic',
|
|
1262
|
+
namespace: 'events',
|
|
1263
|
+
tags: [],
|
|
1264
|
+
metadata: {},
|
|
1265
|
+
accessLevel: 'project',
|
|
1266
|
+
createdAt: Date.now(),
|
|
1267
|
+
updatedAt: Date.now(),
|
|
1268
|
+
version: 1,
|
|
1269
|
+
references: [],
|
|
1270
|
+
accessCount: 0,
|
|
1271
|
+
lastAccessedAt: Date.now(),
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
expect(events.length).toBe(1);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
it('should emit bulkInserted event', async () => {
|
|
1278
|
+
const events: unknown[] = [];
|
|
1279
|
+
backend.on('bulkInserted', (count) => events.push(count));
|
|
1280
|
+
|
|
1281
|
+
await backend.bulkInsert([
|
|
1282
|
+
{
|
|
1283
|
+
id: 'bulk-event-1',
|
|
1284
|
+
key: 'bulk-1',
|
|
1285
|
+
content: 'Bulk content 1',
|
|
1286
|
+
type: 'semantic',
|
|
1287
|
+
namespace: 'events',
|
|
1288
|
+
tags: [],
|
|
1289
|
+
metadata: {},
|
|
1290
|
+
accessLevel: 'project',
|
|
1291
|
+
createdAt: Date.now(),
|
|
1292
|
+
updatedAt: Date.now(),
|
|
1293
|
+
version: 1,
|
|
1294
|
+
references: [],
|
|
1295
|
+
accessCount: 0,
|
|
1296
|
+
lastAccessedAt: Date.now(),
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
id: 'bulk-event-2',
|
|
1300
|
+
key: 'bulk-2',
|
|
1301
|
+
content: 'Bulk content 2',
|
|
1302
|
+
type: 'semantic',
|
|
1303
|
+
namespace: 'events',
|
|
1304
|
+
tags: [],
|
|
1305
|
+
metadata: {},
|
|
1306
|
+
accessLevel: 'project',
|
|
1307
|
+
createdAt: Date.now(),
|
|
1308
|
+
updatedAt: Date.now(),
|
|
1309
|
+
version: 1,
|
|
1310
|
+
references: [],
|
|
1311
|
+
accessCount: 0,
|
|
1312
|
+
lastAccessedAt: Date.now(),
|
|
1313
|
+
},
|
|
1314
|
+
]);
|
|
1315
|
+
|
|
1316
|
+
expect(events).toContain(2);
|
|
1317
|
+
});
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
describe('verbose mode', () => {
|
|
1321
|
+
it('should log when verbose is enabled', async () => {
|
|
1322
|
+
const verboseBackend = createBetterSqlite3Backend({
|
|
1323
|
+
databasePath: ':memory:',
|
|
1324
|
+
verbose: true,
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
// Capture console.log
|
|
1328
|
+
const logs: string[] = [];
|
|
1329
|
+
const originalLog = console.log;
|
|
1330
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
1331
|
+
|
|
1332
|
+
await verboseBackend.initialize();
|
|
1333
|
+
await verboseBackend.rebuildFtsIndex();
|
|
1334
|
+
await verboseBackend.shutdown();
|
|
1335
|
+
|
|
1336
|
+
console.log = originalLog;
|
|
1337
|
+
|
|
1338
|
+
expect(logs.some((l) => l.includes('[BetterSqlite3]'))).toBe(true);
|
|
1339
|
+
});
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
describe('porter tokenizer', () => {
|
|
1343
|
+
it('should support porter tokenizer', async () => {
|
|
1344
|
+
const porterBackend = createBetterSqlite3Backend({
|
|
1345
|
+
databasePath: ':memory:',
|
|
1346
|
+
ftsTokenizer: 'porter',
|
|
1347
|
+
});
|
|
1348
|
+
await porterBackend.initialize();
|
|
1349
|
+
|
|
1350
|
+
expect(porterBackend.getActiveTokenizer()).toBe('porter');
|
|
1351
|
+
expect(porterBackend.isFtsAvailable()).toBe(true);
|
|
1352
|
+
|
|
1353
|
+
// Porter should work for English stemming
|
|
1354
|
+
await porterBackend.store({
|
|
1355
|
+
id: 'porter-1',
|
|
1356
|
+
key: 'running',
|
|
1357
|
+
content: 'The quick brown fox is running',
|
|
1358
|
+
type: 'semantic',
|
|
1359
|
+
namespace: 'porter',
|
|
1360
|
+
tags: [],
|
|
1361
|
+
metadata: {},
|
|
1362
|
+
accessLevel: 'project',
|
|
1363
|
+
createdAt: Date.now(),
|
|
1364
|
+
updatedAt: Date.now(),
|
|
1365
|
+
version: 1,
|
|
1366
|
+
references: [],
|
|
1367
|
+
accessCount: 0,
|
|
1368
|
+
lastAccessedAt: Date.now(),
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
// Porter stemmer should match "run" to "running"
|
|
1372
|
+
const results = await porterBackend.searchFts('run');
|
|
1373
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1374
|
+
|
|
1375
|
+
await porterBackend.shutdown();
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
describe('empty bulk operations', () => {
|
|
1380
|
+
it('should handle empty bulk insert', async () => {
|
|
1381
|
+
await backend.bulkInsert([]);
|
|
1382
|
+
expect(await backend.count()).toBe(0);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
it('should handle empty bulk delete', async () => {
|
|
1386
|
+
const deleted = await backend.bulkDelete([]);
|
|
1387
|
+
expect(deleted).toBe(0);
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
describe('custom tokenizer', () => {
|
|
1392
|
+
it('should report custom tokenizer when configured', async () => {
|
|
1393
|
+
const customBackend = createBetterSqlite3Backend({
|
|
1394
|
+
databasePath: ':memory:',
|
|
1395
|
+
customTokenizer: 'custom_tok',
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
// Note: This will fail to create FTS since custom_tok doesn't exist,
|
|
1399
|
+
// but the tokenizer name should still be reported
|
|
1400
|
+
expect(customBackend.getActiveTokenizer()).toBe('custom_tok');
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
describe('double initialization', () => {
|
|
1405
|
+
it('should handle double initialization', async () => {
|
|
1406
|
+
const newBackend = createBetterSqlite3Backend({
|
|
1407
|
+
databasePath: ':memory:',
|
|
1408
|
+
});
|
|
1409
|
+
|
|
1410
|
+
await newBackend.initialize();
|
|
1411
|
+
await newBackend.initialize(); // Should not throw
|
|
1412
|
+
|
|
1413
|
+
expect(newBackend.isFtsAvailable()).toBe(true);
|
|
1414
|
+
|
|
1415
|
+
await newBackend.shutdown();
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
describe('query with no filters', () => {
|
|
1420
|
+
it('should return all entries with no filters', async () => {
|
|
1421
|
+
await backend.store({
|
|
1422
|
+
id: 'no-filter-1',
|
|
1423
|
+
key: 'key1',
|
|
1424
|
+
content: 'Content 1',
|
|
1425
|
+
type: 'semantic',
|
|
1426
|
+
namespace: 'ns1',
|
|
1427
|
+
tags: [],
|
|
1428
|
+
metadata: {},
|
|
1429
|
+
accessLevel: 'project',
|
|
1430
|
+
createdAt: Date.now(),
|
|
1431
|
+
updatedAt: Date.now(),
|
|
1432
|
+
version: 1,
|
|
1433
|
+
references: [],
|
|
1434
|
+
accessCount: 0,
|
|
1435
|
+
lastAccessedAt: Date.now(),
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
await backend.store({
|
|
1439
|
+
id: 'no-filter-2',
|
|
1440
|
+
key: 'key2',
|
|
1441
|
+
content: 'Content 2',
|
|
1442
|
+
type: 'episodic',
|
|
1443
|
+
namespace: 'ns2',
|
|
1444
|
+
tags: [],
|
|
1445
|
+
metadata: {},
|
|
1446
|
+
accessLevel: 'project',
|
|
1447
|
+
createdAt: Date.now(),
|
|
1448
|
+
updatedAt: Date.now(),
|
|
1449
|
+
version: 1,
|
|
1450
|
+
references: [],
|
|
1451
|
+
accessCount: 0,
|
|
1452
|
+
lastAccessedAt: Date.now(),
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
const results = await backend.query({ type: 'hybrid', limit: 10 });
|
|
1456
|
+
expect(results.length).toBe(2);
|
|
1457
|
+
});
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
describe('delete non-existent', () => {
|
|
1461
|
+
it('should return false when deleting non-existent entry', async () => {
|
|
1462
|
+
const result = await backend.delete('non-existent-id');
|
|
1463
|
+
expect(result).toBe(false);
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
});
|