@polka-codes/cli-shared 0.10.3 → 0.10.6
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/dist/config.d.ts +13 -0
- package/dist/config.js +202 -0
- package/dist/config.js.map +1 -0
- package/dist/config.parameters.test.d.ts +1 -0
- package/dist/config.parameters.test.js +240 -0
- package/dist/config.parameters.test.js.map +1 -0
- package/dist/config.rules.test.d.ts +1 -0
- package/dist/config.rules.test.js +92 -0
- package/dist/config.rules.test.js.map +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +311 -0
- package/dist/config.test.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +694 -70817
- package/dist/index.js.map +1 -0
- package/dist/memory-manager.d.ts +52 -0
- package/dist/memory-manager.js +76 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/project-scope.d.ts +10 -0
- package/dist/project-scope.js +67 -0
- package/dist/project-scope.js.map +1 -0
- package/dist/provider.d.ts +32 -0
- package/dist/provider.js +366 -0
- package/dist/provider.js.map +1 -0
- package/dist/provider.test.d.ts +1 -0
- package/dist/provider.test.js +21 -0
- package/dist/provider.test.js.map +1 -0
- package/dist/sqlite-memory-store.d.ts +112 -0
- package/dist/sqlite-memory-store.js +919 -0
- package/dist/sqlite-memory-store.js.map +1 -0
- package/dist/sqlite-memory-store.test.d.ts +1 -0
- package/dist/sqlite-memory-store.test.js +661 -0
- package/dist/sqlite-memory-store.test.js.map +1 -0
- package/dist/utils/__tests__/parameterSimplifier.test.d.ts +1 -0
- package/dist/utils/__tests__/parameterSimplifier.test.js +137 -0
- package/dist/utils/__tests__/parameterSimplifier.test.js.map +1 -0
- package/dist/utils/checkRipgrep.d.ts +5 -0
- package/dist/utils/checkRipgrep.js +22 -0
- package/dist/utils/checkRipgrep.js.map +1 -0
- package/dist/utils/eventHandler.d.ts +11 -0
- package/dist/utils/eventHandler.js +196 -0
- package/dist/utils/eventHandler.js.map +1 -0
- package/dist/utils/eventHandler.test.d.ts +1 -0
- package/dist/utils/eventHandler.test.js +31 -0
- package/dist/utils/eventHandler.test.js.map +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/listFiles.d.ts +12 -0
- package/dist/utils/listFiles.js +136 -0
- package/dist/utils/listFiles.js.map +1 -0
- package/dist/utils/listFiles.test.d.ts +1 -0
- package/dist/utils/listFiles.test.js +64 -0
- package/dist/utils/listFiles.test.js.map +1 -0
- package/dist/utils/parameterSimplifier.d.ts +1 -0
- package/dist/utils/parameterSimplifier.js +65 -0
- package/dist/utils/parameterSimplifier.js.map +1 -0
- package/dist/utils/readMultiline.d.ts +1 -0
- package/dist/utils/readMultiline.js +19 -0
- package/dist/utils/readMultiline.js.map +1 -0
- package/dist/utils/search.constants.d.ts +7 -0
- package/dist/utils/search.constants.js +8 -0
- package/dist/utils/search.constants.js.map +1 -0
- package/dist/utils/searchFiles.d.ts +12 -0
- package/dist/utils/searchFiles.js +72 -0
- package/dist/utils/searchFiles.js.map +1 -0
- package/dist/utils/searchFiles.test.d.ts +1 -0
- package/dist/utils/searchFiles.test.js +140 -0
- package/dist/utils/searchFiles.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,661 @@
|
|
|
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
|