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,544 @@
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 globals_1 = require("@jest/globals");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const database_1 = require("../../utils/database");
41
+ const database_helper_1 = require("../../test-helpers/database-helper");
42
+ /**
43
+ * Comprehensive test suite for validating watcher migrations at startup
44
+ *
45
+ * This test ensures that the watcher migration system (migrations 004 and 005)
46
+ * are properly applied and all required database components exist.
47
+ *
48
+ * SUCCESS CRITERIA:
49
+ * - All required tables exist (context_changes, context_watchers, deleted_items)
50
+ * - All required triggers are installed and functional
51
+ * - All required indexes are created for performance
52
+ * - Schema matches the expected structure from migration files
53
+ * - Triggers actually fire and record changes correctly
54
+ */
55
+ (0, globals_1.describe)('Watcher Migration Validation Tests', () => {
56
+ let dbManager;
57
+ let db;
58
+ let tempDir;
59
+ let tempDbPath;
60
+ (0, globals_1.beforeEach)(() => {
61
+ // Create fresh test database for each test
62
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-watcher-test-'));
63
+ tempDbPath = path.join(tempDir, 'test.db');
64
+ // Initialize with DatabaseManager which should run all migrations
65
+ dbManager = new database_1.DatabaseManager({ filename: tempDbPath });
66
+ db = dbManager.getDatabase();
67
+ // Register for cleanup
68
+ database_helper_1.TestDatabaseHelper.registerForCleanup(dbManager);
69
+ });
70
+ (0, globals_1.afterEach)(() => {
71
+ try {
72
+ dbManager.close();
73
+ fs.rmSync(tempDir, { recursive: true, force: true });
74
+ }
75
+ catch (error) {
76
+ console.warn('Cleanup error:', error);
77
+ }
78
+ });
79
+ (0, globals_1.describe)('Required Tables Existence', () => {
80
+ (0, globals_1.it)('should have context_changes table with correct schema', () => {
81
+ // Check table exists
82
+ const tables = db
83
+ .prepare(`
84
+ SELECT name FROM sqlite_master
85
+ WHERE type='table' AND name='context_changes'
86
+ `)
87
+ .all();
88
+ (0, globals_1.expect)(tables).toHaveLength(1);
89
+ (0, globals_1.expect)(tables[0]).toEqual({ name: 'context_changes' });
90
+ // Check table schema matches migration 004
91
+ const columns = db.prepare('PRAGMA table_info(context_changes)').all();
92
+ const columnNames = columns.map(col => col.name);
93
+ // Verify all required columns from migration 004
94
+ const expectedColumns = [
95
+ 'sequence_id',
96
+ 'session_id',
97
+ 'item_id',
98
+ 'key',
99
+ 'operation',
100
+ 'old_value',
101
+ 'new_value',
102
+ 'old_metadata',
103
+ 'new_metadata',
104
+ 'category',
105
+ 'priority',
106
+ 'channel',
107
+ 'size_delta',
108
+ 'created_at',
109
+ 'created_by',
110
+ ];
111
+ expectedColumns.forEach(col => {
112
+ (0, globals_1.expect)(columnNames).toContain(col);
113
+ });
114
+ // Check sequence_id is primary key with autoincrement
115
+ const sequenceCol = columns.find(col => col.name === 'sequence_id');
116
+ (0, globals_1.expect)(sequenceCol.pk).toBe(1); // Primary key
117
+ (0, globals_1.expect)(sequenceCol.type).toBe('INTEGER');
118
+ // Check operation constraint
119
+ const operationCol = columns.find(col => col.name === 'operation');
120
+ (0, globals_1.expect)(operationCol.type).toBe('TEXT');
121
+ });
122
+ (0, globals_1.it)('should have context_watchers table with correct schema', () => {
123
+ // Check table exists
124
+ const tables = db
125
+ .prepare(`
126
+ SELECT name FROM sqlite_master
127
+ WHERE type='table' AND name='context_watchers'
128
+ `)
129
+ .all();
130
+ (0, globals_1.expect)(tables).toHaveLength(1);
131
+ // Check table schema
132
+ const columns = db.prepare('PRAGMA table_info(context_watchers)').all();
133
+ const columnNames = columns.map(col => col.name);
134
+ // Verify columns from migration 004
135
+ const expectedColumns = [
136
+ 'id',
137
+ 'session_id',
138
+ 'filter_keys',
139
+ 'filter_categories',
140
+ 'filter_channels',
141
+ 'filter_priorities',
142
+ 'last_sequence',
143
+ 'created_at',
144
+ 'last_poll_at',
145
+ 'expires_at',
146
+ 'metadata',
147
+ ];
148
+ expectedColumns.forEach(col => {
149
+ (0, globals_1.expect)(columnNames).toContain(col);
150
+ });
151
+ // Check id is primary key
152
+ const idCol = columns.find(col => col.name === 'id');
153
+ (0, globals_1.expect)(idCol.pk).toBe(1);
154
+ // Check for is_active column from migration 005
155
+ (0, globals_1.expect)(columnNames).toContain('is_active');
156
+ const isActiveCol = columns.find(col => col.name === 'is_active');
157
+ (0, globals_1.expect)(isActiveCol.dflt_value).toBe('1'); // Default to active
158
+ });
159
+ (0, globals_1.it)('should have deleted_items table from migration 005', () => {
160
+ // Check table exists
161
+ const tables = db
162
+ .prepare(`
163
+ SELECT name FROM sqlite_master
164
+ WHERE type='table' AND name='deleted_items'
165
+ `)
166
+ .all();
167
+ (0, globals_1.expect)(tables).toHaveLength(1);
168
+ // Check table schema
169
+ const columns = db.prepare('PRAGMA table_info(deleted_items)').all();
170
+ const columnNames = columns.map(col => col.name);
171
+ const expectedColumns = [
172
+ 'id',
173
+ 'session_id',
174
+ 'key',
175
+ 'category',
176
+ 'channel',
177
+ 'sequence_number',
178
+ 'deleted_at',
179
+ ];
180
+ expectedColumns.forEach(col => {
181
+ (0, globals_1.expect)(columnNames).toContain(col);
182
+ });
183
+ });
184
+ (0, globals_1.it)('should have sequence_number column added to context_items', () => {
185
+ // Check that context_items table has sequence_number column from migration 005
186
+ const columns = db.prepare('PRAGMA table_info(context_items)').all();
187
+ const columnNames = columns.map(col => col.name);
188
+ (0, globals_1.expect)(columnNames).toContain('sequence_number');
189
+ const seqCol = columns.find(col => col.name === 'sequence_number');
190
+ (0, globals_1.expect)(seqCol.dflt_value).toBe('0'); // Default to 0
191
+ });
192
+ });
193
+ (0, globals_1.describe)('Required Indexes', () => {
194
+ (0, globals_1.it)('should have all performance indexes from migration 004', () => {
195
+ const indexes = db
196
+ .prepare(`
197
+ SELECT name FROM sqlite_master
198
+ WHERE type='index' AND name IN (
199
+ 'idx_changes_sequence',
200
+ 'idx_changes_session_seq',
201
+ 'idx_changes_created',
202
+ 'idx_watchers_expires',
203
+ 'idx_watchers_session'
204
+ )
205
+ `)
206
+ .all();
207
+ const indexNames = indexes.map(idx => idx.name);
208
+ (0, globals_1.expect)(indexNames).toContain('idx_changes_sequence');
209
+ (0, globals_1.expect)(indexNames).toContain('idx_changes_session_seq');
210
+ (0, globals_1.expect)(indexNames).toContain('idx_changes_created');
211
+ (0, globals_1.expect)(indexNames).toContain('idx_watchers_expires');
212
+ (0, globals_1.expect)(indexNames).toContain('idx_watchers_session');
213
+ });
214
+ (0, globals_1.it)('should have indexes from migration 005', () => {
215
+ const indexes = db
216
+ .prepare(`
217
+ SELECT name FROM sqlite_master
218
+ WHERE type='index' AND name IN (
219
+ 'idx_watchers_active',
220
+ 'idx_deleted_items_session',
221
+ 'idx_deleted_items_key'
222
+ )
223
+ `)
224
+ .all();
225
+ const indexNames = indexes.map(idx => idx.name);
226
+ (0, globals_1.expect)(indexNames).toContain('idx_watchers_active');
227
+ (0, globals_1.expect)(indexNames).toContain('idx_deleted_items_session');
228
+ (0, globals_1.expect)(indexNames).toContain('idx_deleted_items_key');
229
+ });
230
+ });
231
+ (0, globals_1.describe)('Required Triggers', () => {
232
+ (0, globals_1.it)('should have change tracking triggers from migration 004', () => {
233
+ const triggers = db
234
+ .prepare(`
235
+ SELECT name FROM sqlite_master
236
+ WHERE type='trigger' AND name IN (
237
+ 'track_context_insert',
238
+ 'track_context_update',
239
+ 'track_context_delete'
240
+ )
241
+ `)
242
+ .all();
243
+ const triggerNames = triggers.map(t => t.name);
244
+ (0, globals_1.expect)(triggerNames).toContain('track_context_insert');
245
+ (0, globals_1.expect)(triggerNames).toContain('track_context_update');
246
+ (0, globals_1.expect)(triggerNames).toContain('track_context_delete');
247
+ (0, globals_1.expect)(triggers).toHaveLength(3);
248
+ });
249
+ (0, globals_1.it)('should have sequence increment triggers from migration 005', () => {
250
+ const triggers = db
251
+ .prepare(`
252
+ SELECT name FROM sqlite_master
253
+ WHERE type='trigger' AND name IN (
254
+ 'increment_sequence_insert',
255
+ 'increment_sequence_update'
256
+ )
257
+ `)
258
+ .all();
259
+ const triggerNames = triggers.map(t => t.name);
260
+ (0, globals_1.expect)(triggerNames).toContain('increment_sequence_insert');
261
+ (0, globals_1.expect)(triggerNames).toContain('increment_sequence_update');
262
+ });
263
+ });
264
+ (0, globals_1.describe)('Functional Trigger Testing', () => {
265
+ (0, globals_1.beforeEach)(() => {
266
+ // Create a test session for trigger testing
267
+ db.prepare(`
268
+ INSERT INTO sessions (id, name, description)
269
+ VALUES ('test-session', 'Test Session', 'Test Description')
270
+ `).run();
271
+ });
272
+ (0, globals_1.it)('should track INSERT operations with track_context_insert trigger', () => {
273
+ // Insert a context item
274
+ db.prepare(`
275
+ INSERT INTO context_items (
276
+ id, session_id, key, value, category, priority, channel, size
277
+ ) VALUES (
278
+ 'test-item-1', 'test-session', 'test-key', 'test-value',
279
+ 'task', 'high', 'default', 10
280
+ )
281
+ `).run();
282
+ // Check that change was tracked
283
+ const changes = db
284
+ .prepare(`
285
+ SELECT * FROM context_changes
286
+ WHERE item_id = 'test-item-1' AND operation = 'CREATE'
287
+ `)
288
+ .all();
289
+ (0, globals_1.expect)(changes).toHaveLength(1);
290
+ const change = changes[0];
291
+ (0, globals_1.expect)(change.session_id).toBe('test-session');
292
+ (0, globals_1.expect)(change.key).toBe('test-key');
293
+ (0, globals_1.expect)(change.operation).toBe('CREATE');
294
+ (0, globals_1.expect)(change.new_value).toBe('test-value');
295
+ (0, globals_1.expect)(change.category).toBe('task');
296
+ (0, globals_1.expect)(change.priority).toBe('high');
297
+ (0, globals_1.expect)(change.channel).toBe('default');
298
+ (0, globals_1.expect)(change.size_delta).toBe(10);
299
+ (0, globals_1.expect)(change.created_by).toBe('context_save');
300
+ });
301
+ (0, globals_1.it)('should track UPDATE operations with track_context_update trigger', () => {
302
+ // Insert initial item
303
+ db.prepare(`
304
+ INSERT INTO context_items (
305
+ id, session_id, key, value, category, priority, size
306
+ ) VALUES (
307
+ 'test-item-2', 'test-session', 'update-key', 'old-value', 'note', 'low', 5
308
+ )
309
+ `).run();
310
+ // Update the item
311
+ db.prepare(`
312
+ UPDATE context_items
313
+ SET value = 'new-value', priority = 'high', size = 15
314
+ WHERE id = 'test-item-2'
315
+ `).run();
316
+ // Check that update was tracked
317
+ const changes = db
318
+ .prepare(`
319
+ SELECT * FROM context_changes
320
+ WHERE item_id = 'test-item-2' AND operation = 'UPDATE'
321
+ `)
322
+ .all();
323
+ (0, globals_1.expect)(changes).toHaveLength(1);
324
+ const change = changes[0];
325
+ (0, globals_1.expect)(change.old_value).toBe('old-value');
326
+ (0, globals_1.expect)(change.new_value).toBe('new-value');
327
+ (0, globals_1.expect)(change.size_delta).toBe(10); // 15 - 5
328
+ (0, globals_1.expect)(change.created_by).toBe('context_save');
329
+ });
330
+ (0, globals_1.it)('should track DELETE operations with track_context_delete trigger', () => {
331
+ // Insert item to delete
332
+ db.prepare(`
333
+ INSERT INTO context_items (
334
+ id, session_id, key, value, category, size
335
+ ) VALUES (
336
+ 'test-item-3', 'test-session', 'delete-key', 'delete-value', 'error', 8
337
+ )
338
+ `).run();
339
+ // Delete the item
340
+ db.prepare(`DELETE FROM context_items WHERE id = 'test-item-3'`).run();
341
+ // Check that deletion was tracked
342
+ const changes = db
343
+ .prepare(`
344
+ SELECT * FROM context_changes
345
+ WHERE item_id = 'test-item-3' AND operation = 'DELETE'
346
+ `)
347
+ .all();
348
+ (0, globals_1.expect)(changes).toHaveLength(1);
349
+ const change = changes[0];
350
+ (0, globals_1.expect)(change.old_value).toBe('delete-value');
351
+ (0, globals_1.expect)(change.new_value).toBeNull();
352
+ (0, globals_1.expect)(change.category).toBe('error');
353
+ (0, globals_1.expect)(change.size_delta).toBe(-8); // Negative for deletion
354
+ (0, globals_1.expect)(change.created_by).toBe('context_delete');
355
+ });
356
+ (0, globals_1.it)('should auto-increment sequence numbers on INSERT', () => {
357
+ // Insert multiple items
358
+ db.prepare(`
359
+ INSERT INTO context_items (
360
+ id, session_id, key, value, sequence_number
361
+ ) VALUES
362
+ ('seq-1', 'test-session', 'key1', 'value1', 0),
363
+ ('seq-2', 'test-session', 'key2', 'value2', 0),
364
+ ('seq-3', 'test-session', 'key3', 'value3', 0)
365
+ `).run();
366
+ // Check sequence numbers were assigned
367
+ const items = db
368
+ .prepare(`
369
+ SELECT id, sequence_number FROM context_items
370
+ WHERE session_id = 'test-session'
371
+ ORDER BY sequence_number
372
+ `)
373
+ .all();
374
+ (0, globals_1.expect)(items).toHaveLength(3);
375
+ (0, globals_1.expect)(items[0].sequence_number).toBeGreaterThan(0);
376
+ (0, globals_1.expect)(items[1].sequence_number).toBeGreaterThan(items[0].sequence_number);
377
+ (0, globals_1.expect)(items[2].sequence_number).toBeGreaterThan(items[1].sequence_number);
378
+ });
379
+ (0, globals_1.it)('should update sequence numbers on significant updates', () => {
380
+ // Insert item
381
+ db.prepare(`
382
+ INSERT INTO context_items (
383
+ id, session_id, key, value, sequence_number
384
+ ) VALUES ('seq-update', 'test-session', 'update-key', 'old-value', 0)
385
+ `).run();
386
+ const originalSeq = db
387
+ .prepare(`
388
+ SELECT sequence_number FROM context_items WHERE id = 'seq-update'
389
+ `)
390
+ .get();
391
+ // Update the value (should trigger sequence increment)
392
+ db.prepare(`
393
+ UPDATE context_items SET value = 'new-value' WHERE id = 'seq-update'
394
+ `).run();
395
+ const newSeq = db
396
+ .prepare(`
397
+ SELECT sequence_number FROM context_items WHERE id = 'seq-update'
398
+ `)
399
+ .get();
400
+ (0, globals_1.expect)(newSeq.sequence_number).toBeGreaterThan(originalSeq.sequence_number);
401
+ });
402
+ });
403
+ (0, globals_1.describe)('Edge Cases and Error Handling', () => {
404
+ (0, globals_1.it)('should handle missing foreign key relationships gracefully', () => {
405
+ // Try to insert change record with non-existent session
406
+ // This should fail due to foreign key constraint
407
+ (0, globals_1.expect)(() => {
408
+ db.prepare(`
409
+ INSERT INTO context_changes (
410
+ session_id, item_id, key, operation, new_value
411
+ ) VALUES ('non-existent-session', 'item1', 'key1', 'CREATE', 'value1')
412
+ `).run();
413
+ }).toThrow();
414
+ });
415
+ (0, globals_1.it)('should enforce operation CHECK constraint', () => {
416
+ // Create session first
417
+ db.prepare(`
418
+ INSERT INTO sessions (id, name) VALUES ('constraint-test', 'Test')
419
+ `).run();
420
+ // Try invalid operation
421
+ (0, globals_1.expect)(() => {
422
+ db.prepare(`
423
+ INSERT INTO context_changes (
424
+ session_id, item_id, key, operation, new_value
425
+ ) VALUES ('constraint-test', 'item1', 'key1', 'INVALID_OP', 'value1')
426
+ `).run();
427
+ }).toThrow();
428
+ // Valid operations should work
429
+ (0, globals_1.expect)(() => {
430
+ db.prepare(`
431
+ INSERT INTO context_changes (
432
+ session_id, item_id, key, operation, new_value
433
+ ) VALUES ('constraint-test', 'item1', 'key1', 'CREATE', 'value1')
434
+ `).run();
435
+ }).not.toThrow();
436
+ });
437
+ (0, globals_1.it)('should handle NULL values correctly in trigger conditions', () => {
438
+ // Create session and item with NULL metadata
439
+ db.prepare(`
440
+ INSERT INTO sessions (id, name) VALUES ('null-test', 'Null Test')
441
+ `).run();
442
+ db.prepare(`
443
+ INSERT INTO context_items (
444
+ id, session_id, key, value, metadata, category
445
+ ) VALUES ('null-item', 'null-test', 'null-key', 'value1', NULL, 'note')
446
+ `).run();
447
+ // Update to non-NULL metadata (should trigger update tracking)
448
+ db.prepare(`
449
+ UPDATE context_items
450
+ SET metadata = '{"test": true}'
451
+ WHERE id = 'null-item'
452
+ `).run();
453
+ // Check that change was tracked
454
+ const changes = db
455
+ .prepare(`
456
+ SELECT * FROM context_changes
457
+ WHERE item_id = 'null-item' AND operation = 'UPDATE'
458
+ `)
459
+ .all();
460
+ (0, globals_1.expect)(changes).toHaveLength(1);
461
+ });
462
+ });
463
+ (0, globals_1.describe)('Migration Completeness', () => {
464
+ (0, globals_1.it)('should have applied all expected migrations', () => {
465
+ // Check migration records exist (if migration table exists)
466
+ const tables = db
467
+ .prepare(`
468
+ SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'
469
+ `)
470
+ .all();
471
+ if (tables.length > 0) {
472
+ const migrations = db
473
+ .prepare(`
474
+ SELECT version FROM migrations
475
+ WHERE version IN ('0.4.0', '0.5.0')
476
+ ORDER BY version
477
+ `)
478
+ .all();
479
+ // Should have both watcher migrations
480
+ (0, globals_1.expect)(migrations.map(m => m.version)).toEqual(['0.4.0', '0.5.0']);
481
+ }
482
+ });
483
+ (0, globals_1.it)('should handle database with partial watcher schema gracefully', () => {
484
+ // This test verifies that migrations are idempotent
485
+ // and can handle databases in various states
486
+ // Check that all expected components exist
487
+ const expectedTables = ['context_changes', 'context_watchers', 'deleted_items'];
488
+ const existingTables = db
489
+ .prepare(`
490
+ SELECT name FROM sqlite_master
491
+ WHERE type='table' AND name IN (${expectedTables.map(() => '?').join(',')})
492
+ `)
493
+ .all(...expectedTables);
494
+ (0, globals_1.expect)(existingTables).toHaveLength(expectedTables.length);
495
+ // Check that all expected triggers exist
496
+ const expectedTriggers = [
497
+ 'track_context_insert',
498
+ 'track_context_update',
499
+ 'track_context_delete',
500
+ 'increment_sequence_insert',
501
+ 'increment_sequence_update',
502
+ ];
503
+ const existingTriggers = db
504
+ .prepare(`
505
+ SELECT name FROM sqlite_master
506
+ WHERE type='trigger' AND name IN (${expectedTriggers.map(() => '?').join(',')})
507
+ `)
508
+ .all(...expectedTriggers);
509
+ (0, globals_1.expect)(existingTriggers).toHaveLength(expectedTriggers.length);
510
+ });
511
+ });
512
+ (0, globals_1.describe)('Performance Validation', () => {
513
+ (0, globals_1.it)('should have indexes that improve query performance', () => {
514
+ // Create test data to verify index usage
515
+ db.prepare(`
516
+ INSERT INTO sessions (id, name) VALUES ('perf-test', 'Performance Test')
517
+ `).run();
518
+ // Insert some test data
519
+ for (let i = 0; i < 100; i++) {
520
+ db.prepare(`
521
+ INSERT INTO context_changes (
522
+ session_id, item_id, key, operation, new_value, created_at
523
+ ) VALUES (
524
+ 'perf-test', 'item-${i}', 'key-${i}', 'CREATE', 'value-${i}',
525
+ datetime('now', '-${i} seconds')
526
+ )
527
+ `).run();
528
+ }
529
+ // Query that should use index
530
+ const explain = db
531
+ .prepare(`
532
+ EXPLAIN QUERY PLAN
533
+ SELECT * FROM context_changes
534
+ WHERE session_id = 'perf-test'
535
+ ORDER BY sequence_id DESC
536
+ LIMIT 10
537
+ `)
538
+ .all();
539
+ // Should mention index usage (not exact string match due to SQLite variations)
540
+ const hasIndexUsage = explain.some(row => row.detail.includes('idx_changes_session_seq') || row.detail.includes('USING INDEX'));
541
+ (0, globals_1.expect)(hasIndexUsage).toBe(true);
542
+ });
543
+ });
544
+ });
@@ -0,0 +1,115 @@
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 globals_1 = require("@jest/globals");
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const database_1 = require("../../utils/database");
41
+ const validation_1 = require("../../utils/validation");
42
+ (0, globals_1.describe)('Security - Input Validation Tests', () => {
43
+ let tempDir;
44
+ let dbManager;
45
+ (0, globals_1.beforeEach)(() => {
46
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-test-'));
47
+ const tempDbPath = path.join(tempDir, 'test.db');
48
+ dbManager = new database_1.DatabaseManager({
49
+ filename: tempDbPath,
50
+ walMode: false,
51
+ });
52
+ });
53
+ (0, globals_1.afterEach)(() => {
54
+ dbManager.close();
55
+ fs.rmSync(tempDir, { recursive: true, force: true });
56
+ });
57
+ (0, globals_1.describe)('Basic SQL Injection Prevention', () => {
58
+ (0, globals_1.it)('should prevent SQL injection in key parameter', () => {
59
+ const db = dbManager.getDatabase();
60
+ const sessionId = 'test-session';
61
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
62
+ const maliciousKey = "key'; DROP TABLE sessions; --";
63
+ db.prepare(`
64
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
65
+ VALUES (?, ?, ?, ?, ?, ?)
66
+ `).run('item-1', sessionId, maliciousKey, 'value', 'test', 'normal');
67
+ const tables = db
68
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'")
69
+ .all();
70
+ (0, globals_1.expect)(tables.length).toBe(1);
71
+ const item = db.prepare('SELECT key FROM context_items WHERE id = ?').get('item-1');
72
+ (0, globals_1.expect)(item.key).toBe(maliciousKey);
73
+ });
74
+ (0, globals_1.it)('should sanitize search queries', () => {
75
+ const maliciousQuery = "test' OR '1'='1";
76
+ const validatedQuery = (0, validation_1.validateSearchQuery)(maliciousQuery);
77
+ (0, globals_1.expect)(validatedQuery).toBe('test OR 1=1');
78
+ });
79
+ });
80
+ (0, globals_1.describe)('Basic Validation', () => {
81
+ (0, globals_1.it)('should validate keys', () => {
82
+ (0, globals_1.expect)(() => (0, validation_1.validateKey)('')).toThrow();
83
+ (0, globals_1.expect)(() => (0, validation_1.validateKey)('valid-key')).not.toThrow();
84
+ });
85
+ (0, globals_1.it)('should validate values', () => {
86
+ (0, globals_1.expect)(() => (0, validation_1.validateValue)('valid value')).not.toThrow();
87
+ // Empty values might be allowed, so just test that function exists
88
+ (0, globals_1.expect)(typeof validation_1.validateValue).toBe('function');
89
+ });
90
+ });
91
+ (0, globals_1.describe)('Resource Management', () => {
92
+ (0, globals_1.it)('should handle small bulk operations', () => {
93
+ const db = dbManager.getDatabase();
94
+ const sessionId = 'test-session';
95
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test');
96
+ const insertStmt = db.prepare(`
97
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
98
+ VALUES (?, ?, ?, ?, ?, ?)
99
+ `);
100
+ const insertMany = db.transaction(() => {
101
+ for (let i = 0; i < 5; i++) {
102
+ insertStmt.run(`bulk-${i}`, sessionId, `key-${i}`, `value-${i}`, 'test', 'normal');
103
+ }
104
+ });
105
+ const start = Date.now();
106
+ insertMany();
107
+ const elapsed = Date.now() - start;
108
+ (0, globals_1.expect)(elapsed).toBeLessThan(1000);
109
+ const count = db
110
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
111
+ .get(sessionId);
112
+ (0, globals_1.expect)(count.count).toBe(5);
113
+ });
114
+ });
115
+ });