@polka-codes/cli-shared 0.10.23 → 0.10.24

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.
Files changed (47) hide show
  1. package/dist/index.js +1913 -7
  2. package/package.json +2 -2
  3. package/dist/config.js +0 -202
  4. package/dist/config.js.map +0 -1
  5. package/dist/config.parameters.test.js +0 -240
  6. package/dist/config.parameters.test.js.map +0 -1
  7. package/dist/config.rules.test.js +0 -92
  8. package/dist/config.rules.test.js.map +0 -1
  9. package/dist/config.test.js +0 -311
  10. package/dist/config.test.js.map +0 -1
  11. package/dist/index.js.map +0 -1
  12. package/dist/memory-manager.js +0 -76
  13. package/dist/memory-manager.js.map +0 -1
  14. package/dist/project-scope.js +0 -67
  15. package/dist/project-scope.js.map +0 -1
  16. package/dist/provider.js +0 -366
  17. package/dist/provider.js.map +0 -1
  18. package/dist/provider.test.js +0 -21
  19. package/dist/provider.test.js.map +0 -1
  20. package/dist/sqlite-memory-store.js +0 -911
  21. package/dist/sqlite-memory-store.js.map +0 -1
  22. package/dist/sqlite-memory-store.test.js +0 -661
  23. package/dist/sqlite-memory-store.test.js.map +0 -1
  24. package/dist/utils/__tests__/parameterSimplifier.test.js +0 -137
  25. package/dist/utils/__tests__/parameterSimplifier.test.js.map +0 -1
  26. package/dist/utils/checkRipgrep.js +0 -22
  27. package/dist/utils/checkRipgrep.js.map +0 -1
  28. package/dist/utils/eventHandler.js +0 -199
  29. package/dist/utils/eventHandler.js.map +0 -1
  30. package/dist/utils/eventHandler.test.js +0 -50
  31. package/dist/utils/eventHandler.test.js.map +0 -1
  32. package/dist/utils/index.js +0 -7
  33. package/dist/utils/index.js.map +0 -1
  34. package/dist/utils/listFiles.js +0 -136
  35. package/dist/utils/listFiles.js.map +0 -1
  36. package/dist/utils/listFiles.test.js +0 -64
  37. package/dist/utils/listFiles.test.js.map +0 -1
  38. package/dist/utils/parameterSimplifier.js +0 -65
  39. package/dist/utils/parameterSimplifier.js.map +0 -1
  40. package/dist/utils/readMultiline.js +0 -19
  41. package/dist/utils/readMultiline.js.map +0 -1
  42. package/dist/utils/search.constants.js +0 -8
  43. package/dist/utils/search.constants.js.map +0 -1
  44. package/dist/utils/searchFiles.js +0 -72
  45. package/dist/utils/searchFiles.js.map +0 -1
  46. package/dist/utils/searchFiles.test.js +0 -140
  47. package/dist/utils/searchFiles.test.js.map +0 -1
@@ -1,661 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
- import { existsSync, unlinkSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
5
- import { SQLiteMemoryStore } from './sqlite-memory-store';
6
- const TEST_DB_PATH = join(tmpdir(), 'test-memory.sqlite');
7
- describe('SQLiteMemoryStore', () => {
8
- let store;
9
- const config = {
10
- enabled: true,
11
- type: 'sqlite',
12
- path: TEST_DB_PATH,
13
- };
14
- beforeEach(() => {
15
- // Clean up any existing test database
16
- if (existsSync(TEST_DB_PATH)) {
17
- unlinkSync(TEST_DB_PATH);
18
- }
19
- store = new SQLiteMemoryStore(config, 'project:/tmp/test-project');
20
- });
21
- afterEach(async () => {
22
- await store.close();
23
- // Clean up test database
24
- if (existsSync(TEST_DB_PATH)) {
25
- unlinkSync(TEST_DB_PATH);
26
- }
27
- });
28
- describe('CRUD Operations', () => {
29
- it('should create and read memory entry', async () => {
30
- await store.updateMemory('replace', 'test-entry', 'Test content', {
31
- entry_type: 'note',
32
- });
33
- const content = await store.readMemory('test-entry');
34
- expect(content).toBe('Test content');
35
- });
36
- it('should update existing entry', async () => {
37
- await store.updateMemory('replace', 'test-entry', 'Original content', {
38
- entry_type: 'note',
39
- });
40
- await store.updateMemory('replace', 'test-entry', 'Updated content', {
41
- entry_type: 'note',
42
- });
43
- const content = await store.readMemory('test-entry');
44
- expect(content).toBe('Updated content');
45
- });
46
- it('should append to existing entry', async () => {
47
- await store.updateMemory('replace', 'test-entry', 'Line 1', {
48
- entry_type: 'note',
49
- });
50
- await store.updateMemory('append', 'test-entry', 'Line 2', {
51
- entry_type: 'note',
52
- });
53
- const content = await store.readMemory('test-entry');
54
- expect(content).toBe('Line 1\nLine 2');
55
- });
56
- it('should delete memory entry', async () => {
57
- await store.updateMemory('replace', 'test-entry', 'Test content', {
58
- entry_type: 'note',
59
- });
60
- await store.updateMemory('remove', 'test-entry', undefined);
61
- const content = await store.readMemory('test-entry');
62
- expect(content).toBeUndefined();
63
- });
64
- it('should return undefined for non-existent entry', async () => {
65
- const content = await store.readMemory('non-existent');
66
- expect(content).toBeUndefined();
67
- });
68
- it('should preserve metadata across updates', async () => {
69
- await store.updateMemory('replace', 'test-entry', 'Content', {
70
- entry_type: 'todo',
71
- status: 'open',
72
- priority: 'high',
73
- tags: 'bug,urgent',
74
- });
75
- await store.updateMemory('append', 'test-entry', ' - more info');
76
- const content = await store.readMemory('test-entry');
77
- expect(content).toBe('Content\n - more info');
78
- const entries = await store.queryMemory({ search: 'test-entry' }, { operation: 'select' });
79
- expect(Array.isArray(entries)).toBe(true);
80
- expect(entries).toHaveLength(1);
81
- const entryArray = entries;
82
- expect(entryArray[0].entry_type).toBe('todo');
83
- expect(entryArray[0].status).toBe('open');
84
- expect(entryArray[0].priority).toBe('high');
85
- expect(entryArray[0].tags).toBe('bug,urgent');
86
- });
87
- });
88
- describe('Query Functionality', () => {
89
- beforeEach(async () => {
90
- // Setup test data with delays to ensure unique timestamps
91
- await store.updateMemory('replace', 'todo-1', 'Fix login bug', {
92
- entry_type: 'todo',
93
- status: 'open',
94
- priority: 'high',
95
- tags: 'bug,auth',
96
- });
97
- await new Promise((resolve) => setTimeout(resolve, 2));
98
- await store.updateMemory('replace', 'todo-2', 'Add tests', {
99
- entry_type: 'todo',
100
- status: 'open',
101
- priority: 'medium',
102
- tags: 'testing',
103
- });
104
- await new Promise((resolve) => setTimeout(resolve, 2));
105
- await store.updateMemory('replace', 'todo-3', 'Refactor code', {
106
- entry_type: 'todo',
107
- status: 'done',
108
- priority: 'low',
109
- tags: 'cleanup',
110
- });
111
- await new Promise((resolve) => setTimeout(resolve, 2));
112
- await store.updateMemory('replace', 'bug-1', 'Security issue', {
113
- entry_type: 'bug',
114
- status: 'open',
115
- priority: 'critical',
116
- tags: 'security',
117
- });
118
- await new Promise((resolve) => setTimeout(resolve, 2));
119
- await store.updateMemory('replace', 'note-1', 'Meeting notes', {
120
- entry_type: 'note',
121
- status: undefined,
122
- priority: undefined,
123
- tags: 'documentation',
124
- });
125
- });
126
- it('should query all entries', async () => {
127
- const entries = await store.queryMemory({}, { operation: 'select' });
128
- expect(entries).toHaveLength(5);
129
- });
130
- it('should filter by type', async () => {
131
- const todos = await store.queryMemory({ type: 'todo' }, { operation: 'select' });
132
- expect(todos).toHaveLength(3);
133
- const bugs = await store.queryMemory({ type: 'bug' }, { operation: 'select' });
134
- expect(bugs).toHaveLength(1);
135
- });
136
- it('should filter by status', async () => {
137
- const open = await store.queryMemory({ status: 'open' }, { operation: 'select' });
138
- expect(open).toHaveLength(3);
139
- const done = await store.queryMemory({ status: 'done' }, { operation: 'select' });
140
- expect(done).toHaveLength(1);
141
- });
142
- it('should filter by priority', async () => {
143
- const high = await store.queryMemory({ priority: 'high' }, { operation: 'select' });
144
- expect(high).toHaveLength(1);
145
- const critical = await store.queryMemory({ priority: 'critical' }, { operation: 'select' });
146
- expect(critical).toHaveLength(1);
147
- });
148
- it('should filter by tags', async () => {
149
- const bugs = await store.queryMemory({ tags: 'bug' }, { operation: 'select' });
150
- expect(bugs).toHaveLength(1);
151
- });
152
- it('should search in content and name', async () => {
153
- const results = await store.queryMemory({ search: 'login' }, { operation: 'select' });
154
- expect(Array.isArray(results)).toBe(true);
155
- expect(results).toHaveLength(1);
156
- const resultArray = results;
157
- expect(resultArray[0].name).toBe('todo-1');
158
- });
159
- it('should combine multiple filters', async () => {
160
- const results = await store.queryMemory({ type: 'todo', status: 'open' }, { operation: 'select' });
161
- expect(Array.isArray(results)).toBe(true);
162
- expect(results).toHaveLength(2);
163
- });
164
- it('should sort results', async () => {
165
- const results = await store.queryMemory({ sortBy: 'updated', sortOrder: 'desc' }, { operation: 'select' });
166
- expect(Array.isArray(results)).toBe(true);
167
- expect(results).toHaveLength(5);
168
- const resultArray = results;
169
- // Last created should be first
170
- expect(resultArray[0].name).toBe('note-1');
171
- });
172
- it('should limit results', async () => {
173
- const results = await store.queryMemory({ limit: 2 }, { operation: 'select' });
174
- expect(Array.isArray(results)).toBe(true);
175
- expect(results).toHaveLength(2);
176
- });
177
- it('should offset results', async () => {
178
- const page1 = await store.queryMemory({ limit: 2, offset: 0 }, { operation: 'select' });
179
- const page2 = await store.queryMemory({ limit: 2, offset: 2 }, { operation: 'select' });
180
- expect(Array.isArray(page1)).toBe(true);
181
- expect(Array.isArray(page2)).toBe(true);
182
- expect(page1).toHaveLength(2);
183
- expect(page2).toHaveLength(2);
184
- const page1Array = page1;
185
- const page2Array = page2;
186
- expect(page1Array[0].name).not.toBe(page2Array[0].name);
187
- });
188
- it('should count results', async () => {
189
- const count = await store.queryMemory({ type: 'todo' }, { operation: 'count' });
190
- expect(count).toBe(3);
191
- });
192
- });
193
- describe('Security and Validation', () => {
194
- it('should reject empty type', async () => {
195
- await expect(async () => {
196
- await store.queryMemory({ type: ' ' }, { operation: 'select' });
197
- }).toThrow('Type cannot be empty');
198
- });
199
- it('should reject invalid priority', async () => {
200
- await expect(async () => {
201
- await store.queryMemory({ priority: 'invalid' }, { operation: 'select' });
202
- }).toThrow('Invalid priority');
203
- });
204
- it('should reject invalid tags', async () => {
205
- await expect(async () => {
206
- await store.queryMemory({ tags: ' ' }, { operation: 'select' });
207
- }).toThrow('Tags cannot be empty');
208
- });
209
- it('should reject invalid limit', async () => {
210
- await expect(async () => {
211
- await store.queryMemory({ limit: -1 }, { operation: 'select' });
212
- }).toThrow('Limit must be between 1 and 10000');
213
- await expect(async () => {
214
- await store.queryMemory({ limit: 10001 }, { operation: 'select' });
215
- }).toThrow('Limit must be between 1 and 10000');
216
- });
217
- it('should reject invalid offset', async () => {
218
- await expect(async () => {
219
- await store.queryMemory({ offset: -1 }, { operation: 'select' });
220
- }).toThrow('Offset must be >= 0');
221
- });
222
- it('should reject invalid sortBy', async () => {
223
- await expect(async () => {
224
- await store.queryMemory({ sortBy: 'invalid' }, { operation: 'select' });
225
- }).toThrow('Invalid sortBy');
226
- });
227
- it('should reject invalid sortOrder', async () => {
228
- await expect(async () => {
229
- await store.queryMemory({ sortBy: 'created', sortOrder: 'invalid' }, { operation: 'select' });
230
- }).toThrow('Invalid sortOrder');
231
- });
232
- it('should sanitize search terms to prevent LIKE injection', async () => {
233
- await store.updateMemory('replace', 'test-1', 'Test content', {
234
- entry_type: 'note',
235
- });
236
- // This should not cause SQL errors
237
- const results = await store.queryMemory({ search: '%_' }, { operation: 'select' });
238
- expect(Array.isArray(results)).toBe(true);
239
- });
240
- it('should validate path does not escape home directory', async () => {
241
- const configWithBadPath = {
242
- enabled: true,
243
- type: 'sqlite',
244
- path: '~/.config/polka-codes/../../../etc/passwd',
245
- };
246
- const badStore = new SQLiteMemoryStore(configWithBadPath, 'project:/tmp/test');
247
- await expect(async () => {
248
- await badStore.updateMemory('replace', 'test', 'content', { entry_type: 'note' });
249
- }).toThrow();
250
- badStore.close();
251
- });
252
- });
253
- describe('Edge Cases', () => {
254
- it('should handle special characters in content', async () => {
255
- const specialContent = `
256
- <script>alert("xss")</script>
257
- SQL: ' OR "1"="1
258
- Quotes: "double" and 'single'
259
- Newlines: \n\n
260
- Tabs: \t\t
261
- `;
262
- await store.updateMemory('replace', 'special-chars', specialContent, {
263
- entry_type: 'note',
264
- });
265
- const content = await store.readMemory('special-chars');
266
- expect(content).toBe(specialContent);
267
- });
268
- it('should handle very long content', async () => {
269
- const longContent = 'x'.repeat(100000); // 100KB
270
- await store.updateMemory('replace', 'long-content', longContent, {
271
- entry_type: 'note',
272
- });
273
- const content = await store.readMemory('long-content');
274
- expect(content).toHaveLength(100000);
275
- });
276
- it('should handle unicode content', async () => {
277
- const unicodeContent = 'Hello 世界 🌍 🎉 emoji test';
278
- await store.updateMemory('replace', 'unicode', unicodeContent, {
279
- entry_type: 'note',
280
- });
281
- const content = await store.readMemory('unicode');
282
- expect(content).toBe(unicodeContent);
283
- });
284
- it('should handle concurrent operations', async () => {
285
- const operations = Array.from({ length: 10 }, (_, i) => store.updateMemory('replace', `concurrent-${i}`, `Content ${i}`, {
286
- entry_type: 'note',
287
- }));
288
- await Promise.all(operations);
289
- const entries = await store.queryMemory({}, { operation: 'select' });
290
- expect(entries).toHaveLength(10);
291
- });
292
- it('should handle empty content error', async () => {
293
- await expect(async () => {
294
- await store.updateMemory('replace', 'test', '', { entry_type: 'note' });
295
- }).toThrow();
296
- });
297
- it('should handle default topic name', async () => {
298
- await store.updateMemory('replace', ':default:', 'Default content', {
299
- entry_type: 'note',
300
- });
301
- const content = await store.readMemory(':default:');
302
- expect(content).toBe('Default content');
303
- });
304
- });
305
- describe('Database Statistics', () => {
306
- it('should return correct statistics', async () => {
307
- await store.updateMemory('replace', 'todo-1', 'Content', { entry_type: 'todo' });
308
- await store.updateMemory('replace', 'todo-2', 'Content', { entry_type: 'todo' });
309
- await store.updateMemory('replace', 'bug-1', 'Content', { entry_type: 'bug' });
310
- const stats = await store.getStats();
311
- expect(stats.totalEntries).toBe(3);
312
- expect(stats.entriesByType.todo).toBe(2);
313
- expect(stats.entriesByType.bug).toBe(1);
314
- expect(stats.databaseSize).toBeGreaterThan(0);
315
- });
316
- it('should return zero statistics for empty database', async () => {
317
- const stats = await store.getStats();
318
- expect(stats.totalEntries).toBe(0);
319
- expect(stats.entriesByType).toEqual({});
320
- });
321
- });
322
- describe('Transaction Safety', () => {
323
- it('should rollback on error', async () => {
324
- await store.updateMemory('replace', 'test-1', 'Content 1', {
325
- entry_type: 'note',
326
- });
327
- // This should fail
328
- try {
329
- await store.transaction(async () => {
330
- await store.updateMemory('replace', 'test-2', 'Content 2', {
331
- entry_type: 'note',
332
- });
333
- throw new Error('Intentional error');
334
- });
335
- }
336
- catch {
337
- // Expected
338
- }
339
- // test-2 should not exist
340
- const content = await store.readMemory('test-2');
341
- expect(content).toBeUndefined();
342
- });
343
- });
344
- describe('Scope Management', () => {
345
- it('should isolate entries by scope', async () => {
346
- const store1 = new SQLiteMemoryStore(config, 'project:/tmp/project-1');
347
- const store2 = new SQLiteMemoryStore(config, 'project:/tmp/project-2');
348
- try {
349
- await store1.updateMemory('replace', 'same-name', 'Project 1 content', {
350
- entry_type: 'note',
351
- });
352
- await store2.updateMemory('replace', 'same-name', 'Project 2 content', {
353
- entry_type: 'note',
354
- });
355
- const content1 = await store1.readMemory('same-name');
356
- const content2 = await store2.readMemory('same-name');
357
- expect(content1).toBe('Project 1 content');
358
- expect(content2).toBe('Project 2 content');
359
- }
360
- finally {
361
- await store1.close();
362
- await store2.close();
363
- }
364
- });
365
- it('should use the scope passed to constructor for queries', async () => {
366
- const testStore = new SQLiteMemoryStore(config, 'project:/tmp/test-scope');
367
- await testStore.updateMemory('replace', 'test', 'content', { entry_type: 'note' });
368
- const entries = await testStore.queryMemory({ scope: 'project' }, { operation: 'select' });
369
- expect(Array.isArray(entries)).toBe(true);
370
- expect(entries).toHaveLength(1);
371
- const entryArray = entries;
372
- expect(entryArray[0].scope).toBe('project:/tmp/test-scope');
373
- await testStore.close();
374
- });
375
- });
376
- describe('Database Recovery', () => {
377
- it('should backup corrupted database', async () => {
378
- // Write initial data
379
- await store.updateMemory('replace', 'test-1', 'Content', { entry_type: 'note' });
380
- // Corrupt the database by writing garbage
381
- const { writeFileSync } = require('node:fs');
382
- writeFileSync(TEST_DB_PATH, 'corrupted data');
383
- // This should backup and recreate
384
- const newStore = new SQLiteMemoryStore(config, 'project:/tmp/test-project');
385
- try {
386
- // Should work without error
387
- await newStore.updateMemory('replace', 'test-2', 'New content', {
388
- entry_type: 'note',
389
- });
390
- const content = await newStore.readMemory('test-2');
391
- expect(content).toBe('New content');
392
- // Old data should be gone
393
- const oldContent = await newStore.readMemory('test-1');
394
- expect(oldContent).toBeUndefined();
395
- }
396
- finally {
397
- await newStore.close();
398
- }
399
- });
400
- });
401
- describe('Tag Filtering Improvements', () => {
402
- beforeEach(async () => {
403
- // Create test entries with various tag formats
404
- await store.updateMemory('replace', 'entry1', 'Content 1', {
405
- entry_type: 'note',
406
- tags: 'fix-bug',
407
- });
408
- await store.updateMemory('replace', 'entry2', 'Content 2', {
409
- entry_type: 'note',
410
- tags: 'fix-bug,high-priority',
411
- });
412
- await store.updateMemory('replace', 'entry3', 'Content 3', {
413
- entry_type: 'note',
414
- tags: 'high-priority',
415
- });
416
- await store.updateMemory('replace', 'entry4', 'Content 4', {
417
- entry_type: 'note',
418
- tags: 'feature',
419
- });
420
- });
421
- it('should filter by exact tag match', async () => {
422
- const results = await store.queryMemory({ tags: 'fix-bug' }, { operation: 'select' });
423
- expect(Array.isArray(results)).toBe(true);
424
- expect(results).toHaveLength(2);
425
- const resultsArray = results;
426
- expect(resultsArray.map((r) => r.name)).toContain('entry1');
427
- expect(resultsArray.map((r) => r.name)).toContain('entry2');
428
- });
429
- it('should filter by tag at beginning of comma list', async () => {
430
- const results = await store.queryMemory({ tags: 'high-priority' }, { operation: 'select' });
431
- expect(Array.isArray(results)).toBe(true);
432
- expect(results).toHaveLength(2);
433
- const resultsArray = results;
434
- expect(resultsArray.map((r) => r.name)).toContain('entry2');
435
- expect(resultsArray.map((r) => r.name)).toContain('entry3');
436
- });
437
- it('should not match partial tags', async () => {
438
- // Should not match "fix-bug" when searching for "bug"
439
- const results = await store.queryMemory({ tags: 'bug' }, { operation: 'select' });
440
- expect(Array.isArray(results)).toBe(true);
441
- expect(results).toHaveLength(0);
442
- });
443
- it('should handle tags with spaces', async () => {
444
- await store.updateMemory('replace', 'entry5', 'Content 5', {
445
- entry_type: 'note',
446
- tags: 'fix bug',
447
- });
448
- const results = await store.queryMemory({ tags: 'fix bug' }, { operation: 'select' });
449
- expect(Array.isArray(results)).toBe(true);
450
- expect(results).toHaveLength(1);
451
- const resultArray = results;
452
- expect(resultArray[0].name).toBe('entry5');
453
- });
454
- });
455
- describe('Batch Update Memory', () => {
456
- it('should perform multiple operations atomically in a single transaction', async () => {
457
- // Create initial entries
458
- await store.updateMemory('replace', 'entry1', 'Content 1', {
459
- entry_type: 'note',
460
- tags: 'tag1',
461
- });
462
- await store.updateMemory('replace', 'entry2', 'Content 2', {
463
- entry_type: 'note',
464
- tags: 'tag2',
465
- });
466
- // Perform batch update
467
- await store.batchUpdateMemory([
468
- {
469
- operation: 'replace',
470
- name: 'entry1',
471
- content: 'Updated Content 1',
472
- metadata: { entry_type: 'note', tags: 'updated-tag1' },
473
- },
474
- {
475
- operation: 'replace',
476
- name: 'entry2',
477
- content: 'Updated Content 2',
478
- metadata: { entry_type: 'note', tags: 'updated-tag2' },
479
- },
480
- {
481
- operation: 'replace',
482
- name: 'entry3',
483
- content: 'New Content 3',
484
- metadata: { entry_type: 'note', tags: 'tag3' },
485
- },
486
- ]);
487
- // Verify all updates were applied
488
- const entry1 = await store.queryMemory({ search: 'entry1' }, { operation: 'select' });
489
- const entry2 = await store.queryMemory({ search: 'entry2' }, { operation: 'select' });
490
- const entry3 = await store.queryMemory({ search: 'entry3' }, { operation: 'select' });
491
- expect(Array.isArray(entry1)).toBe(true);
492
- expect(Array.isArray(entry2)).toBe(true);
493
- expect(Array.isArray(entry3)).toBe(true);
494
- expect(entry1).toHaveLength(1);
495
- const entry1Array = entry1;
496
- expect(entry1Array[0].content).toBe('Updated Content 1');
497
- expect(entry1Array[0].tags).toBe('updated-tag1');
498
- expect(entry2).toHaveLength(1);
499
- const entry2Array = entry2;
500
- expect(entry2Array[0].content).toBe('Updated Content 2');
501
- expect(entry2Array[0].tags).toBe('updated-tag2');
502
- expect(entry3).toHaveLength(1);
503
- const entry3Array = entry3;
504
- expect(entry3Array[0].content).toBe('New Content 3');
505
- expect(entry3Array[0].tags).toBe('tag3');
506
- });
507
- it('should support atomic rename using batch operations', async () => {
508
- // Create entry with metadata
509
- await store.updateMemory('replace', 'old-name', 'Content', {
510
- entry_type: 'note',
511
- status: 'active',
512
- priority: 'high',
513
- tags: 'important',
514
- });
515
- // Perform atomic rename using batch operations
516
- await store.batchUpdateMemory([
517
- {
518
- operation: 'replace',
519
- name: 'new-name',
520
- content: 'Content',
521
- metadata: { entry_type: 'note', status: 'active', priority: 'high', tags: 'important' },
522
- },
523
- { operation: 'remove', name: 'old-name' },
524
- ]);
525
- // Verify rename worked and metadata was preserved
526
- const oldEntry = await store.queryMemory({ search: 'old-name' }, { operation: 'select' });
527
- const newEntry = await store.queryMemory({ search: 'new-name' }, { operation: 'select' });
528
- expect(Array.isArray(oldEntry)).toBe(true);
529
- expect(Array.isArray(newEntry)).toBe(true);
530
- expect(oldEntry).toHaveLength(0);
531
- expect(newEntry).toHaveLength(1);
532
- const newEntryArray = newEntry;
533
- expect(newEntryArray[0].content).toBe('Content');
534
- expect(newEntryArray[0].entry_type).toBe('note');
535
- expect(newEntryArray[0].status).toBe('active');
536
- expect(newEntryArray[0].priority).toBe('high');
537
- expect(newEntryArray[0].tags).toBe('important');
538
- });
539
- });
540
- describe('Lock File Cleanup', () => {
541
- it('should clean up old lock files', async () => {
542
- const { readdir } = await import('node:fs/promises');
543
- const { writeFile } = await import('node:fs/promises');
544
- const { rmSync, existsSync: fsExistsSync } = await import('node:fs');
545
- // Create a dedicated temp directory for this test
546
- const uniqueId = Date.now() + Math.random();
547
- const testDir = join(tmpdir(), `test-cleanup-${uniqueId}`);
548
- const { mkdirSync } = await import('node:fs');
549
- mkdirSync(testDir, { recursive: true });
550
- try {
551
- // Create some old lock files (> 10 min threshold)
552
- const oldTimestamp = Date.now() - 700000; // ~11.5 minutes ago
553
- const testDbName = 'test.db';
554
- await writeFile(`${testDir}/${testDbName}.lock.released.${oldTimestamp}`, 'old released lock');
555
- await writeFile(`${testDir}/${testDbName}.lock.stale.${oldTimestamp}`, 'old stale lock');
556
- await writeFile(`${testDir}/${testDbName}.lock.invalid.${oldTimestamp}`, 'old invalid lock');
557
- await writeFile(`${testDir}/${testDbName}.lock.corrupt.${oldTimestamp}`, 'old corrupt lock');
558
- // Create recent lock file that should not be deleted
559
- const recentTimestamp = Date.now() - 300000; // 5 minutes ago (< 10 min threshold)
560
- await writeFile(`${testDir}/${testDbName}.lock.released.${recentTimestamp}`, 'recent released lock');
561
- // Reset cleanup throttle to ensure cleanup runs in this test
562
- SQLiteMemoryStore.resetCleanupThrottle();
563
- // Trigger cleanup by initializing database
564
- const cleanupConfig = {
565
- enabled: true,
566
- type: 'sqlite',
567
- path: `${testDir}/${testDbName}`,
568
- };
569
- const cleanupStore = new SQLiteMemoryStore(cleanupConfig, 'project:/tmp/test-cleanup');
570
- await cleanupStore.updateMemory('replace', 'test', 'content');
571
- // Wait for background cleanup to complete (cleanup runs in background via .catch())
572
- await new Promise((resolve) => setTimeout(resolve, 1000));
573
- // Check that old files are removed but recent one remains
574
- const files = await readdir(testDir);
575
- const oldLockFiles = files.filter((f) => f.startsWith(testDbName) && f.includes('.lock.') && f.includes(`${oldTimestamp}`));
576
- const recentLockFiles = files.filter((f) => f.startsWith(testDbName) && f.includes('.lock.released.') && f.includes(`${recentTimestamp}`));
577
- expect(oldLockFiles).toHaveLength(0);
578
- expect(recentLockFiles).toHaveLength(1);
579
- await cleanupStore.close();
580
- }
581
- finally {
582
- // Clean up test directory
583
- if (fsExistsSync(testDir)) {
584
- rmSync(testDir, { recursive: true, force: true });
585
- }
586
- }
587
- });
588
- it('should not delete lock files younger than max age', async () => {
589
- const { writeFile } = await import('node:fs/promises');
590
- const { readdir } = await import('node:fs/promises');
591
- const { rmSync, existsSync: fsExistsSync } = await import('node:fs');
592
- const { mkdirSync } = await import('node:fs');
593
- // Create a dedicated temp directory for this test
594
- const uniqueId = Date.now() + Math.random();
595
- const testDir = join(tmpdir(), `test-recent-${uniqueId}`);
596
- mkdirSync(testDir, { recursive: true });
597
- try {
598
- const testDbName = 'test.db';
599
- const recentTimestamp = Date.now() - 300000; // 5 minutes ago (< 10 min threshold)
600
- // Create recent lock files
601
- await writeFile(`${testDir}/${testDbName}.lock.released.${recentTimestamp}`, 'recent lock');
602
- await writeFile(`${testDir}/${testDbName}.lock.stale.${recentTimestamp}`, 'recent stale');
603
- // Reset cleanup throttle to ensure cleanup runs in this test
604
- SQLiteMemoryStore.resetCleanupThrottle();
605
- // Trigger cleanup
606
- const cleanupConfig = {
607
- enabled: true,
608
- type: 'sqlite',
609
- path: `${testDir}/${testDbName}`,
610
- };
611
- const cleanupStore = new SQLiteMemoryStore(cleanupConfig, 'project:/tmp/test-cleanup-recent');
612
- await cleanupStore.updateMemory('replace', 'test2', 'content2');
613
- // Wait for background cleanup to complete
614
- await new Promise((resolve) => setTimeout(resolve, 1000));
615
- // Check that recent files still exist
616
- const files = await readdir(testDir);
617
- const recentLockFiles = files.filter((f) => f.startsWith(testDbName) && f.includes('.lock.') && f.includes(`${recentTimestamp}`));
618
- expect(recentLockFiles.length).toBeGreaterThanOrEqual(2);
619
- await cleanupStore.close();
620
- }
621
- finally {
622
- // Clean up test directory
623
- if (fsExistsSync(testDir)) {
624
- rmSync(testDir, { recursive: true, force: true });
625
- }
626
- }
627
- });
628
- it('should handle cleanup errors gracefully', async () => {
629
- const { writeFile } = await import('node:fs/promises');
630
- const { rmSync, existsSync: fsExistsSync } = await import('node:fs');
631
- const { mkdirSync } = await import('node:fs');
632
- // Create a dedicated temp directory for this test
633
- const uniqueId = Date.now() + Math.random();
634
- const testDir = join(tmpdir(), `test-error-${uniqueId}`);
635
- mkdirSync(testDir, { recursive: true });
636
- try {
637
- // Create an invalid lock file (should not cause crash)
638
- const invalidFile = `${testDir}/invalid-lock-file.txt`;
639
- await writeFile(invalidFile, 'not a lock file');
640
- // Reset cleanup throttle to ensure cleanup runs in this test
641
- SQLiteMemoryStore.resetCleanupThrottle();
642
- // This should not throw even with invalid files present
643
- const cleanupConfig = {
644
- enabled: true,
645
- type: 'sqlite',
646
- path: `${testDir}/test.db`,
647
- };
648
- const cleanupStore = new SQLiteMemoryStore(cleanupConfig, 'project:/tmp/test-cleanup-error');
649
- await cleanupStore.updateMemory('replace', 'test3', 'content3');
650
- await cleanupStore.close();
651
- }
652
- finally {
653
- // Clean up test directory
654
- if (fsExistsSync(testDir)) {
655
- rmSync(testDir, { recursive: true, force: true });
656
- }
657
- }
658
- });
659
- });
660
- });
661
- //# sourceMappingURL=sqlite-memory-store.test.js.map