mcp-memory-keeper 0.10.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const database_1 = require("../../utils/database");
37
+ const git_1 = require("../../utils/git");
38
+ const validation_1 = require("../../utils/validation");
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const uuid_1 = require("uuid");
43
+ describe('Error Cases Integration Tests', () => {
44
+ let dbManager;
45
+ let tempDbPath;
46
+ let db;
47
+ beforeEach(() => {
48
+ tempDbPath = path.join(os.tmpdir(), `test-errors-${Date.now()}.db`);
49
+ dbManager = new database_1.DatabaseManager({
50
+ filename: tempDbPath,
51
+ maxSize: 10 * 1024 * 1024,
52
+ walMode: true,
53
+ });
54
+ db = dbManager.getDatabase();
55
+ });
56
+ afterEach(() => {
57
+ dbManager.close();
58
+ try {
59
+ fs.unlinkSync(tempDbPath);
60
+ fs.unlinkSync(`${tempDbPath}-wal`);
61
+ fs.unlinkSync(`${tempDbPath}-shm`);
62
+ }
63
+ catch (_e) {
64
+ // Ignore
65
+ }
66
+ });
67
+ describe('Database errors', () => {
68
+ it('should handle database full errors', () => {
69
+ // Create a tiny database manager
70
+ const tinyDbPath = path.join(os.tmpdir(), `tiny-db-${Date.now()}.db`);
71
+ const tinyDb = new database_1.DatabaseManager({
72
+ filename: tinyDbPath,
73
+ maxSize: 1024, // 1KB - very small
74
+ walMode: true,
75
+ });
76
+ const tinyDbConn = tinyDb.getDatabase();
77
+ const sessionId = (0, uuid_1.v4)();
78
+ tinyDbConn.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
79
+ // Try to fill it up
80
+ let errorThrown = false;
81
+ try {
82
+ for (let i = 0; i < 1000; i++) {
83
+ if (tinyDb.isDatabaseFull()) {
84
+ errorThrown = true;
85
+ break;
86
+ }
87
+ tinyDbConn
88
+ .prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)')
89
+ .run((0, uuid_1.v4)(), sessionId, `key${i}`, 'A'.repeat(100));
90
+ }
91
+ }
92
+ catch (_e) {
93
+ errorThrown = true;
94
+ }
95
+ expect(errorThrown).toBe(true);
96
+ tinyDb.close();
97
+ fs.unlinkSync(tinyDbPath);
98
+ });
99
+ it('should handle constraint violations', () => {
100
+ const sessionId = (0, uuid_1.v4)();
101
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
102
+ // Insert item
103
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sessionId, 'unique_key', 'value1');
104
+ // Try to insert duplicate key (violates unique constraint)
105
+ expect(() => {
106
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sessionId, 'unique_key', 'value2');
107
+ }).toThrow();
108
+ });
109
+ it('should handle foreign key violations', () => {
110
+ // Try to insert context item with non-existent session
111
+ expect(() => {
112
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), 'non-existent-session', 'key', 'value');
113
+ }).toThrow();
114
+ });
115
+ it('should handle transaction rollbacks', () => {
116
+ const sessionId = (0, uuid_1.v4)();
117
+ expect(() => {
118
+ dbManager.transaction(() => {
119
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
120
+ // This will fail due to foreign key constraint
121
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), 'invalid-checkpoint', 'invalid-item');
122
+ });
123
+ }).toThrow();
124
+ // Verify session was not created (rolled back)
125
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
126
+ expect(session).toBeFalsy();
127
+ });
128
+ });
129
+ describe('Input validation errors', () => {
130
+ it('should reject invalid session names', () => {
131
+ const invalidNames = [
132
+ '',
133
+ ' ',
134
+ '\n\t',
135
+ 'a'.repeat(256), // Too long
136
+ '../../../etc/passwd',
137
+ 'session\0name',
138
+ 'session<script>alert(1)</script>',
139
+ ];
140
+ invalidNames.forEach(name => {
141
+ expect(() => (0, validation_1.validateSessionName)(name)).toThrow();
142
+ });
143
+ });
144
+ it('should reject invalid file paths', () => {
145
+ const invalidPaths = [
146
+ '../../../etc/passwd',
147
+ '/etc/passwd',
148
+ 'C:\\Windows\\System32\\config\\sam',
149
+ '\\\\server\\share\\file',
150
+ 'file\0name.txt',
151
+ '',
152
+ 'con.txt', // Windows reserved name
153
+ 'prn.txt', // Windows reserved name
154
+ ];
155
+ invalidPaths.forEach(filePath => {
156
+ expect(() => (0, validation_1.validateFilePath)(filePath, 'read')).toThrow();
157
+ });
158
+ });
159
+ it('should sanitize search queries', () => {
160
+ const dangerousQueries = [
161
+ "'; DROP TABLE sessions; --",
162
+ '%" OR "1"="1',
163
+ "' UNION SELECT * FROM sessions --",
164
+ '%_test_%',
165
+ ];
166
+ dangerousQueries.forEach(query => {
167
+ const sanitized = (0, validation_1.validateSearchQuery)(query);
168
+ expect(sanitized).not.toContain("'");
169
+ expect(sanitized).not.toContain('"');
170
+ expect(sanitized).not.toContain(';');
171
+ expect(sanitized).not.toContain('--');
172
+ });
173
+ });
174
+ it('should handle null and undefined inputs', () => {
175
+ expect(() => (0, validation_1.validateSessionName)(null)).toThrow();
176
+ expect(() => (0, validation_1.validateSessionName)(undefined)).toThrow();
177
+ expect(() => (0, validation_1.validateFilePath)(null, 'read')).toThrow();
178
+ expect(() => (0, validation_1.validateFilePath)(undefined, 'read')).toThrow();
179
+ });
180
+ });
181
+ describe('File system errors', () => {
182
+ it('should handle non-existent file reads', () => {
183
+ const sessionId = (0, uuid_1.v4)();
184
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
185
+ const nonExistentPath = path.join(os.tmpdir(), 'non-existent-file.txt');
186
+ // Simulate file read error
187
+ let errorMessage = '';
188
+ try {
189
+ const _content = fs.readFileSync(nonExistentPath, 'utf-8');
190
+ }
191
+ catch (e) {
192
+ errorMessage = e.message;
193
+ }
194
+ expect(errorMessage).toContain('ENOENT');
195
+ });
196
+ it('should handle permission errors', () => {
197
+ // This test is platform-specific and may need adjustment
198
+ const restrictedPath = '/root/test.txt'; // Usually no write permission
199
+ let errorThrown = false;
200
+ let errorCode = '';
201
+ try {
202
+ fs.writeFileSync(restrictedPath, 'test');
203
+ }
204
+ catch (e) {
205
+ errorThrown = true;
206
+ errorCode = e.code;
207
+ }
208
+ expect(errorThrown).toBe(true);
209
+ // Accept ENOENT (parent dir doesn't exist) or EACCES/EPERM (no permission)
210
+ expect(['EACCES', 'EPERM', 'ENOENT']).toContain(errorCode);
211
+ });
212
+ it('should handle disk space errors', () => {
213
+ // This is difficult to test reliably across platforms
214
+ // We'll simulate the behavior
215
+ const mockWriteLargeFile = (path, size) => {
216
+ try {
217
+ // Check available space (platform specific)
218
+ const availableSpace = 1024 * 1024 * 100; // Mock 100MB available
219
+ if (size > availableSpace) {
220
+ return { success: false, error: 'ENOSPC: no space left on device' };
221
+ }
222
+ return { success: true };
223
+ }
224
+ catch (e) {
225
+ return { success: false, error: e.message };
226
+ }
227
+ };
228
+ const result = mockWriteLargeFile('/tmp/large.file', 1024 * 1024 * 1024); // 1GB
229
+ expect(result.success).toBe(false);
230
+ expect(result.error).toContain('ENOSPC');
231
+ });
232
+ });
233
+ describe('Git operation errors', () => {
234
+ it('should handle corrupted git repository', async () => {
235
+ const corruptRepoPath = path.join(os.tmpdir(), `corrupt-repo-${Date.now()}`);
236
+ fs.mkdirSync(corruptRepoPath, { recursive: true });
237
+ // Create a fake .git directory with corrupted content
238
+ const gitDir = path.join(corruptRepoPath, '.git');
239
+ fs.mkdirSync(gitDir);
240
+ fs.writeFileSync(path.join(gitDir, 'HEAD'), 'corrupted content');
241
+ const gitOps = new git_1.GitOperations(corruptRepoPath);
242
+ const info = await gitOps.getGitInfo();
243
+ expect(info.isGitRepo).toBe(false);
244
+ expect(info.status).toContain('error');
245
+ fs.rmSync(corruptRepoPath, { recursive: true, force: true });
246
+ });
247
+ it('should handle git command timeouts', async () => {
248
+ // Mock a slow git operation
249
+ const mockSlowGitOp = async (timeout) => {
250
+ return new Promise(resolve => {
251
+ const timer = setTimeout(() => {
252
+ resolve({ success: true });
253
+ }, timeout + 1000);
254
+ setTimeout(() => {
255
+ clearTimeout(timer);
256
+ resolve({ success: false, error: 'Operation timed out' });
257
+ }, timeout);
258
+ });
259
+ };
260
+ const result = await mockSlowGitOp(1000);
261
+ expect(result.success).toBe(false);
262
+ expect(result.error).toContain('timed out');
263
+ });
264
+ });
265
+ describe('Concurrent access errors', () => {
266
+ it('should handle database lock timeouts', async () => {
267
+ // Create two database connections
268
+ const db2Manager = new database_1.DatabaseManager({
269
+ filename: tempDbPath,
270
+ maxSize: 10 * 1024 * 1024,
271
+ walMode: true,
272
+ });
273
+ const db2 = db2Manager.getDatabase();
274
+ const sessionId = (0, uuid_1.v4)();
275
+ // Start a transaction in first connection
276
+ const _transaction1 = db.transaction(() => {
277
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
278
+ // Simulate long-running operation
279
+ const start = Date.now();
280
+ while (Date.now() - start < 100) {
281
+ // Busy wait
282
+ }
283
+ });
284
+ // Try to write in second connection (should succeed with WAL mode)
285
+ let secondWriteSucceeded = false;
286
+ try {
287
+ db2.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run((0, uuid_1.v4)(), 'Test2');
288
+ secondWriteSucceeded = true;
289
+ }
290
+ catch (_e) {
291
+ // In WAL mode, this should not throw
292
+ }
293
+ expect(secondWriteSucceeded).toBe(true);
294
+ db2Manager.close();
295
+ });
296
+ it('should handle multiple readers', () => {
297
+ const sessionId = (0, uuid_1.v4)();
298
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
299
+ // Create multiple readers
300
+ const readers = [];
301
+ for (let i = 0; i < 5; i++) {
302
+ const reader = new database_1.DatabaseManager({
303
+ filename: tempDbPath,
304
+ maxSize: 10 * 1024 * 1024,
305
+ walMode: true,
306
+ });
307
+ readers.push(reader);
308
+ }
309
+ // All readers should be able to read simultaneously
310
+ const results = readers.map(reader => {
311
+ const db = reader.getDatabase();
312
+ return db.prepare('SELECT * FROM sessions WHERE id = ?').get(sessionId);
313
+ });
314
+ expect(results.every(r => r !== undefined)).toBe(true);
315
+ // Cleanup
316
+ readers.forEach(r => r.close());
317
+ });
318
+ });
319
+ describe('Memory errors', () => {
320
+ it('should handle large result sets', () => {
321
+ const sessionId = (0, uuid_1.v4)();
322
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Large Dataset');
323
+ // Insert many items
324
+ const stmt = db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)');
325
+ for (let i = 0; i < 10000; i++) {
326
+ stmt.run((0, uuid_1.v4)(), sessionId, `key${i}`, `value${i}`);
327
+ }
328
+ // Use iterating to handle large result sets
329
+ const stmt2 = db.prepare('SELECT * FROM context_items WHERE session_id = ?');
330
+ let count = 0;
331
+ for (const _row of stmt2.iterate(sessionId)) {
332
+ count++;
333
+ if (count > 5000)
334
+ break; // Limit iteration
335
+ }
336
+ expect(count).toBeGreaterThan(5000);
337
+ });
338
+ it('should handle very long strings', () => {
339
+ const sessionId = (0, uuid_1.v4)();
340
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Long String Test');
341
+ const veryLongString = 'A'.repeat(1024 * 1024); // 1MB string
342
+ // Should handle gracefully
343
+ let errorThrown = false;
344
+ try {
345
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sessionId, 'long_key', veryLongString);
346
+ }
347
+ catch (_e) {
348
+ errorThrown = true;
349
+ }
350
+ // SQLite can handle large strings, so this should work
351
+ expect(errorThrown).toBe(false);
352
+ });
353
+ });
354
+ describe('Recovery scenarios', () => {
355
+ it('should recover from corrupted context items', () => {
356
+ const sessionId = (0, uuid_1.v4)();
357
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Recovery Test');
358
+ // Insert valid items
359
+ for (let i = 0; i < 5; i++) {
360
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sessionId, `key${i}`, `value${i}`);
361
+ }
362
+ // Simulate corrupted metadata
363
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, metadata) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), sessionId, 'corrupted', 'value', '{invalid json');
364
+ // SQLite 3.9+ has json_valid function, check if available
365
+ let validItems;
366
+ try {
367
+ // Try with json_valid
368
+ validItems = db
369
+ .prepare(`SELECT * FROM context_items
370
+ WHERE session_id = ?
371
+ AND (metadata IS NULL OR json_valid(metadata))`)
372
+ .all(sessionId);
373
+ }
374
+ catch (_e) {
375
+ // Fallback without json_valid
376
+ validItems = db
377
+ .prepare(`SELECT * FROM context_items
378
+ WHERE session_id = ?
379
+ AND metadata IS NULL`)
380
+ .all(sessionId);
381
+ }
382
+ expect(validItems).toHaveLength(5); // Only valid items
383
+ });
384
+ it('should handle checkpoint restoration failures gracefully', () => {
385
+ const sessionId = (0, uuid_1.v4)();
386
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Checkpoint Recovery');
387
+ // Create checkpoint with missing references
388
+ const checkpointId = (0, uuid_1.v4)();
389
+ db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, sessionId, 'Broken Checkpoint');
390
+ // Temporarily disable foreign keys to simulate corruption
391
+ db.pragma('foreign_keys = OFF');
392
+ // Add checkpoint items referencing non-existent context items
393
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, 'non-existent-item');
394
+ // Re-enable foreign keys
395
+ db.pragma('foreign_keys = ON');
396
+ // Restoration should handle missing items
397
+ const restorable = db
398
+ .prepare(`
399
+ SELECT ci.* FROM context_items ci
400
+ JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
401
+ WHERE cpi.checkpoint_id = ?
402
+ `)
403
+ .all(checkpointId);
404
+ expect(restorable).toHaveLength(0); // No items to restore
405
+ });
406
+ });
407
+ });