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,908 @@
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 database_1 = require("../../utils/database");
38
+ const ContextRepository_1 = require("../../repositories/ContextRepository");
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
+ const validation_1 = require("../../utils/validation");
44
+ (0, globals_1.describe)('Context Reassign Channel Handler Integration Tests', () => {
45
+ let dbManager;
46
+ let tempDbPath;
47
+ let db;
48
+ let _contextRepo;
49
+ let testSessionId;
50
+ let secondSessionId;
51
+ (0, globals_1.beforeEach)(() => {
52
+ tempDbPath = path.join(os.tmpdir(), `test-context-reassign-channel-${Date.now()}.db`);
53
+ dbManager = new database_1.DatabaseManager({
54
+ filename: tempDbPath,
55
+ maxSize: 10 * 1024 * 1024,
56
+ walMode: true,
57
+ });
58
+ db = dbManager.getDatabase();
59
+ _contextRepo = new ContextRepository_1.ContextRepository(dbManager);
60
+ // Create test sessions
61
+ testSessionId = (0, uuid_1.v4)();
62
+ secondSessionId = (0, uuid_1.v4)();
63
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
64
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(secondSessionId, 'Second Session');
65
+ });
66
+ (0, globals_1.afterEach)(() => {
67
+ dbManager.close();
68
+ try {
69
+ fs.unlinkSync(tempDbPath);
70
+ fs.unlinkSync(`${tempDbPath}-wal`);
71
+ fs.unlinkSync(`${tempDbPath}-shm`);
72
+ }
73
+ catch (_e) {
74
+ // Ignore
75
+ }
76
+ });
77
+ function createTestData() {
78
+ const items = [
79
+ // Main channel items
80
+ {
81
+ key: 'config.database.url',
82
+ value: 'postgresql://localhost:5432/myapp',
83
+ category: 'config',
84
+ priority: 'high',
85
+ channel: 'main',
86
+ },
87
+ {
88
+ key: 'config.cache.ttl',
89
+ value: '3600',
90
+ category: 'config',
91
+ priority: 'normal',
92
+ channel: 'main',
93
+ },
94
+ {
95
+ key: 'task.deploy.status',
96
+ value: 'completed',
97
+ category: 'task',
98
+ priority: 'high',
99
+ channel: 'main',
100
+ },
101
+ // Feature branch items
102
+ {
103
+ key: 'feature.auth.enabled',
104
+ value: 'true',
105
+ category: 'config',
106
+ priority: 'high',
107
+ channel: 'feature/auth',
108
+ },
109
+ {
110
+ key: 'feature.auth.provider',
111
+ value: 'oauth2',
112
+ category: 'config',
113
+ priority: 'normal',
114
+ channel: 'feature/auth',
115
+ },
116
+ {
117
+ key: 'task.auth.implement',
118
+ value: 'in_progress',
119
+ category: 'task',
120
+ priority: 'high',
121
+ channel: 'feature/auth',
122
+ },
123
+ // Development channel items
124
+ {
125
+ key: 'dev.debug.enabled',
126
+ value: 'true',
127
+ category: 'config',
128
+ priority: 'low',
129
+ channel: 'development',
130
+ },
131
+ {
132
+ key: 'dev.log.level',
133
+ value: 'debug',
134
+ category: 'config',
135
+ priority: 'low',
136
+ channel: 'development',
137
+ },
138
+ // Private item
139
+ {
140
+ key: 'secret.api.key',
141
+ value: 'sk-1234567890',
142
+ category: 'config',
143
+ priority: 'high',
144
+ channel: 'secure',
145
+ is_private: 1,
146
+ },
147
+ // Item from another session
148
+ {
149
+ key: 'other.session.item',
150
+ value: 'Not accessible',
151
+ category: 'note',
152
+ priority: 'normal',
153
+ channel: 'main',
154
+ session_id: secondSessionId,
155
+ },
156
+ ];
157
+ const stmt = db.prepare(`
158
+ INSERT INTO context_items (
159
+ id, session_id, key, value, category, priority, channel, is_private
160
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
161
+ `);
162
+ items.forEach(item => {
163
+ stmt.run((0, uuid_1.v4)(), item.session_id || testSessionId, item.key, item.value, item.category, item.priority, item.channel, item.is_private || 0);
164
+ });
165
+ }
166
+ (0, globals_1.describe)('Reassign by Specific Keys', () => {
167
+ (0, globals_1.beforeEach)(() => {
168
+ createTestData();
169
+ });
170
+ (0, globals_1.it)('should reassign specific keys to a new channel', () => {
171
+ const keysToMove = ['config.database.url', 'config.cache.ttl'];
172
+ const newChannel = 'production';
173
+ // Simulate handler logic
174
+ const updateStmt = db.prepare(`
175
+ UPDATE context_items
176
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
177
+ WHERE session_id = ? AND key IN (${keysToMove.map(() => '?').join(',')})
178
+ `);
179
+ const result = updateStmt.run(newChannel, testSessionId, ...keysToMove);
180
+ (0, globals_1.expect)(result.changes).toBe(2);
181
+ // Verify the changes
182
+ const movedItems = db
183
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
184
+ .all(testSessionId, newChannel);
185
+ (0, globals_1.expect)(movedItems.length).toBe(2);
186
+ (0, globals_1.expect)(movedItems.every((item) => item.channel === newChannel)).toBe(true);
187
+ (0, globals_1.expect)(movedItems.map((item) => item.key).sort()).toEqual(keysToMove.sort());
188
+ // Handler response
189
+ const handlerResponse = {
190
+ content: [
191
+ {
192
+ type: 'text',
193
+ text: JSON.stringify({
194
+ operation: 'reassign_channel',
195
+ keys: keysToMove,
196
+ newChannel: newChannel,
197
+ itemsUpdated: result.changes,
198
+ success: true,
199
+ }, null, 2),
200
+ },
201
+ ],
202
+ };
203
+ const parsed = JSON.parse(handlerResponse.content[0].text);
204
+ (0, globals_1.expect)(parsed.itemsUpdated).toBe(2);
205
+ (0, globals_1.expect)(parsed.success).toBe(true);
206
+ });
207
+ (0, globals_1.it)('should handle non-existent keys gracefully', () => {
208
+ const keysToMove = ['non.existent.key1', 'non.existent.key2'];
209
+ const newChannel = 'production';
210
+ const updateStmt = db.prepare(`
211
+ UPDATE context_items
212
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
213
+ WHERE session_id = ? AND key IN (${keysToMove.map(() => '?').join(',')})
214
+ `);
215
+ const result = updateStmt.run(newChannel, testSessionId, ...keysToMove);
216
+ (0, globals_1.expect)(result.changes).toBe(0);
217
+ // Handler response
218
+ const handlerResponse = {
219
+ content: [
220
+ {
221
+ type: 'text',
222
+ text: JSON.stringify({
223
+ operation: 'reassign_channel',
224
+ keys: keysToMove,
225
+ newChannel: newChannel,
226
+ itemsUpdated: 0,
227
+ warning: 'No items found matching the specified keys',
228
+ }, null, 2),
229
+ },
230
+ ],
231
+ };
232
+ const parsed = JSON.parse(handlerResponse.content[0].text);
233
+ (0, globals_1.expect)(parsed.itemsUpdated).toBe(0);
234
+ (0, globals_1.expect)(parsed.warning).toBeTruthy();
235
+ });
236
+ (0, globals_1.it)('should not reassign items from other sessions', () => {
237
+ const keysToMove = ['other.session.item'];
238
+ const newChannel = 'production';
239
+ const updateStmt = db.prepare(`
240
+ UPDATE context_items
241
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
242
+ WHERE session_id = ? AND key IN (${keysToMove.map(() => '?').join(',')})
243
+ `);
244
+ const result = updateStmt.run(newChannel, testSessionId, ...keysToMove);
245
+ (0, globals_1.expect)(result.changes).toBe(0);
246
+ // Verify item wasn't moved
247
+ const item = db
248
+ .prepare('SELECT * FROM context_items WHERE key = ?')
249
+ .get('other.session.item');
250
+ (0, globals_1.expect)(item.channel).toBe('main'); // Original channel
251
+ (0, globals_1.expect)(item.session_id).toBe(secondSessionId);
252
+ });
253
+ (0, globals_1.it)('should handle empty keys array', () => {
254
+ const keysToMove = [];
255
+ const _newChannel = 'production';
256
+ // Handler should validate input
257
+ try {
258
+ if (keysToMove.length === 0) {
259
+ throw new validation_1.ValidationError('Keys array cannot be empty');
260
+ }
261
+ }
262
+ catch (_error) {
263
+ (0, globals_1.expect)(_error).toBeInstanceOf(validation_1.ValidationError);
264
+ (0, globals_1.expect)(_error.message).toContain('Keys array cannot be empty');
265
+ }
266
+ });
267
+ });
268
+ (0, globals_1.describe)('Reassign by Key Pattern', () => {
269
+ (0, globals_1.beforeEach)(() => {
270
+ createTestData();
271
+ });
272
+ (0, globals_1.it)('should reassign items matching key pattern', () => {
273
+ const keyPattern = 'config.*';
274
+ const newChannel = 'configuration';
275
+ // Convert pattern to SQL GLOB pattern
276
+ const globPattern = keyPattern.replace(/\*/g, '%');
277
+ const updateStmt = db.prepare(`
278
+ UPDATE context_items
279
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
280
+ WHERE session_id = ? AND key LIKE ?
281
+ `);
282
+ const result = updateStmt.run(newChannel, testSessionId, globPattern);
283
+ // Should update config.database.url and config.cache.ttl (not feature.auth.* or dev.*)
284
+ (0, globals_1.expect)(result.changes).toBe(2);
285
+ // Verify the changes
286
+ const movedItems = db
287
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
288
+ .all(testSessionId, newChannel);
289
+ (0, globals_1.expect)(movedItems.length).toBe(2);
290
+ (0, globals_1.expect)(movedItems.every((item) => item.key.startsWith('config.'))).toBe(true);
291
+ });
292
+ (0, globals_1.it)('should handle complex patterns', () => {
293
+ const keyPattern = 'feature.*.enabled';
294
+ const newChannel = 'feature-flags';
295
+ // This pattern should match keys like feature.auth.enabled
296
+ const updateStmt = db.prepare(`
297
+ UPDATE context_items
298
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
299
+ WHERE session_id = ? AND key GLOB ?
300
+ `);
301
+ const result = updateStmt.run(newChannel, testSessionId, keyPattern);
302
+ (0, globals_1.expect)(result.changes).toBe(1);
303
+ const movedItem = db
304
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
305
+ .get(testSessionId, 'feature.auth.enabled');
306
+ (0, globals_1.expect)(movedItem.channel).toBe('feature-flags');
307
+ });
308
+ (0, globals_1.it)('should combine pattern with other filters', () => {
309
+ const keyPattern = '*.*';
310
+ const category = 'config';
311
+ const newChannel = 'settings';
312
+ const updateStmt = db.prepare(`
313
+ UPDATE context_items
314
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
315
+ WHERE session_id = ? AND key GLOB ? AND category = ?
316
+ `);
317
+ const _result = updateStmt.run(newChannel, testSessionId, keyPattern, category);
318
+ // Should update all config items with dot notation keys
319
+ const movedItems = db
320
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
321
+ .all(testSessionId, newChannel);
322
+ (0, globals_1.expect)(movedItems.every((item) => item.category === 'config')).toBe(true);
323
+ (0, globals_1.expect)(movedItems.every((item) => item.key.includes('.'))).toBe(true);
324
+ });
325
+ (0, globals_1.it)('should handle pattern with no matches', () => {
326
+ const keyPattern = 'nonexistent.*';
327
+ const newChannel = 'nowhere';
328
+ const updateStmt = db.prepare(`
329
+ UPDATE context_items
330
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
331
+ WHERE session_id = ? AND key GLOB ?
332
+ `);
333
+ const result = updateStmt.run(newChannel, testSessionId, keyPattern);
334
+ (0, globals_1.expect)(result.changes).toBe(0);
335
+ // Handler response
336
+ const handlerResponse = {
337
+ content: [
338
+ {
339
+ type: 'text',
340
+ text: JSON.stringify({
341
+ operation: 'reassign_channel',
342
+ keyPattern: keyPattern,
343
+ newChannel: newChannel,
344
+ itemsUpdated: 0,
345
+ warning: 'No items found matching the pattern',
346
+ }, null, 2),
347
+ },
348
+ ],
349
+ };
350
+ const parsed = JSON.parse(handlerResponse.content[0].text);
351
+ (0, globals_1.expect)(parsed.warning).toBeTruthy();
352
+ });
353
+ });
354
+ (0, globals_1.describe)('Reassign Entire Channel', () => {
355
+ (0, globals_1.beforeEach)(() => {
356
+ createTestData();
357
+ });
358
+ (0, globals_1.it)('should move all items from one channel to another', () => {
359
+ const fromChannel = 'feature/auth';
360
+ const toChannel = 'release/v1.0';
361
+ const updateStmt = db.prepare(`
362
+ UPDATE context_items
363
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
364
+ WHERE session_id = ? AND channel = ?
365
+ `);
366
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
367
+ (0, globals_1.expect)(result.changes).toBe(3); // All feature/auth items
368
+ // Verify no items remain in old channel
369
+ const oldChannelItems = db
370
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
371
+ .all(testSessionId, fromChannel);
372
+ (0, globals_1.expect)(oldChannelItems.length).toBe(0);
373
+ // Verify all items moved to new channel
374
+ const newChannelItems = db
375
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
376
+ .all(testSessionId, toChannel);
377
+ (0, globals_1.expect)(newChannelItems.length).toBe(3);
378
+ });
379
+ (0, globals_1.it)('should handle channel merge conflicts', () => {
380
+ // Add an item to target channel first
381
+ db.prepare(`
382
+ INSERT INTO context_items (id, session_id, key, value, channel)
383
+ VALUES (?, ?, ?, ?, ?)
384
+ `).run((0, uuid_1.v4)(), testSessionId, 'existing.item', 'Already in production', 'production');
385
+ const fromChannel = 'main';
386
+ const toChannel = 'production';
387
+ // Get count before merge
388
+ const beforeCount = db
389
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND channel = ?')
390
+ .get(testSessionId, toChannel).count;
391
+ const updateStmt = db.prepare(`
392
+ UPDATE context_items
393
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
394
+ WHERE session_id = ? AND channel = ?
395
+ `);
396
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
397
+ // Get count after merge
398
+ const afterCount = db
399
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND channel = ?')
400
+ .get(testSessionId, toChannel).count;
401
+ (0, globals_1.expect)(afterCount).toBe(beforeCount + result.changes);
402
+ });
403
+ (0, globals_1.it)('should not move items when source channel is empty', () => {
404
+ const fromChannel = 'non-existent-channel';
405
+ const toChannel = 'production';
406
+ const updateStmt = db.prepare(`
407
+ UPDATE context_items
408
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
409
+ WHERE session_id = ? AND channel = ?
410
+ `);
411
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
412
+ (0, globals_1.expect)(result.changes).toBe(0);
413
+ });
414
+ });
415
+ (0, globals_1.describe)('Filtered Reassignment', () => {
416
+ (0, globals_1.beforeEach)(() => {
417
+ createTestData();
418
+ });
419
+ (0, globals_1.it)('should reassign with category filter', () => {
420
+ const fromChannel = 'main';
421
+ const toChannel = 'tasks';
422
+ const category = 'task';
423
+ const updateStmt = db.prepare(`
424
+ UPDATE context_items
425
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
426
+ WHERE session_id = ? AND channel = ? AND category = ?
427
+ `);
428
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel, category);
429
+ (0, globals_1.expect)(result.changes).toBe(1); // Only task.deploy.status
430
+ // Verify only tasks moved
431
+ const movedItems = db
432
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
433
+ .all(testSessionId, toChannel);
434
+ (0, globals_1.expect)(movedItems.every((item) => item.category === 'task')).toBe(true);
435
+ // Verify config items stayed in main
436
+ const remainingItems = db
437
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
438
+ .all(testSessionId, fromChannel);
439
+ (0, globals_1.expect)(remainingItems.some((item) => item.category === 'config')).toBe(true);
440
+ });
441
+ (0, globals_1.it)('should reassign with priority filter', () => {
442
+ const fromChannel = 'feature/auth';
443
+ const toChannel = 'critical';
444
+ const priorities = ['high'];
445
+ const placeholders = priorities.map(() => '?').join(',');
446
+ const updateStmt = db.prepare(`
447
+ UPDATE context_items
448
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
449
+ WHERE session_id = ? AND channel = ? AND priority IN (${placeholders})
450
+ `);
451
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel, ...priorities);
452
+ (0, globals_1.expect)(result.changes).toBe(2); // feature.auth.enabled and task.auth.implement
453
+ // Verify only high priority items moved
454
+ const movedItems = db
455
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
456
+ .all(testSessionId, toChannel);
457
+ (0, globals_1.expect)(movedItems.every((item) => item.priority === 'high')).toBe(true);
458
+ });
459
+ (0, globals_1.it)('should reassign with multiple filters combined', () => {
460
+ const keyPattern = 'feature.*';
461
+ const category = 'config';
462
+ const priorities = ['high', 'normal'];
463
+ const newChannel = 'feature-config';
464
+ const placeholders = priorities.map(() => '?').join(',');
465
+ const updateStmt = db.prepare(`
466
+ UPDATE context_items
467
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
468
+ WHERE session_id = ?
469
+ AND key GLOB ?
470
+ AND category = ?
471
+ AND priority IN (${placeholders})
472
+ `);
473
+ const result = updateStmt.run(newChannel, testSessionId, keyPattern, category, ...priorities);
474
+ // Should match feature.auth.enabled and feature.auth.provider
475
+ (0, globals_1.expect)(result.changes).toBe(2);
476
+ const movedItems = db
477
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
478
+ .all(testSessionId, newChannel);
479
+ (0, globals_1.expect)(movedItems.every((item) => item.key.startsWith('feature.'))).toBe(true);
480
+ (0, globals_1.expect)(movedItems.every((item) => item.category === 'config')).toBe(true);
481
+ });
482
+ });
483
+ (0, globals_1.describe)('Dry Run Support', () => {
484
+ (0, globals_1.beforeEach)(() => {
485
+ createTestData();
486
+ });
487
+ (0, globals_1.it)('should preview changes without applying them', () => {
488
+ const fromChannel = 'main';
489
+ const toChannel = 'production';
490
+ const _dryRun = true;
491
+ // In dry run, we SELECT instead of UPDATE
492
+ const previewStmt = db.prepare(`
493
+ SELECT id, key, value, category, priority, channel
494
+ FROM context_items
495
+ WHERE session_id = ? AND channel = ?
496
+ `);
497
+ const itemsToMove = previewStmt.all(testSessionId, fromChannel);
498
+ (0, globals_1.expect)(itemsToMove.length).toBe(3);
499
+ // Verify no actual changes were made
500
+ const originalItems = db
501
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
502
+ .all(testSessionId, fromChannel);
503
+ (0, globals_1.expect)(originalItems.length).toBe(3); // Still in original channel
504
+ // Handler response for dry run
505
+ const handlerResponse = {
506
+ content: [
507
+ {
508
+ type: 'text',
509
+ text: JSON.stringify({
510
+ operation: 'reassign_channel',
511
+ dryRun: true,
512
+ fromChannel: fromChannel,
513
+ toChannel: toChannel,
514
+ itemsToMove: itemsToMove.map((item) => ({
515
+ key: item.key,
516
+ value: item.value.substring(0, 50) + (item.value.length > 50 ? '...' : ''),
517
+ category: item.category,
518
+ priority: item.priority,
519
+ })),
520
+ totalItems: itemsToMove.length,
521
+ }, null, 2),
522
+ },
523
+ ],
524
+ };
525
+ const parsed = JSON.parse(handlerResponse.content[0].text);
526
+ (0, globals_1.expect)(parsed.dryRun).toBe(true);
527
+ (0, globals_1.expect)(parsed.totalItems).toBe(3);
528
+ (0, globals_1.expect)(parsed.itemsToMove).toHaveLength(3);
529
+ });
530
+ (0, globals_1.it)('should preview pattern-based reassignment', () => {
531
+ const keyPattern = 'config.*';
532
+ const newChannel = 'settings';
533
+ const _dryRun = true;
534
+ const previewStmt = db.prepare(`
535
+ SELECT id, key, value, category, priority, channel
536
+ FROM context_items
537
+ WHERE session_id = ? AND key LIKE ?
538
+ `);
539
+ const itemsToMove = previewStmt.all(testSessionId, keyPattern.replace(/\*/g, '%'));
540
+ (0, globals_1.expect)(itemsToMove.length).toBe(2);
541
+ // Handler response
542
+ const handlerResponse = {
543
+ content: [
544
+ {
545
+ type: 'text',
546
+ text: JSON.stringify({
547
+ operation: 'reassign_channel',
548
+ dryRun: true,
549
+ keyPattern: keyPattern,
550
+ newChannel: newChannel,
551
+ itemsToMove: itemsToMove.map((item) => ({
552
+ key: item.key,
553
+ currentChannel: item.channel,
554
+ category: item.category,
555
+ priority: item.priority,
556
+ })),
557
+ totalItems: itemsToMove.length,
558
+ }, null, 2),
559
+ },
560
+ ],
561
+ };
562
+ const parsed = JSON.parse(handlerResponse.content[0].text);
563
+ (0, globals_1.expect)(parsed.totalItems).toBe(2);
564
+ (0, globals_1.expect)(parsed.itemsToMove.every((item) => item.key.startsWith('config.'))).toBe(true);
565
+ });
566
+ (0, globals_1.it)('should show empty preview when no matches', () => {
567
+ const keyPattern = 'nonexistent.*';
568
+ const newChannel = 'nowhere';
569
+ const _dryRun = true;
570
+ const previewStmt = db.prepare(`
571
+ SELECT id, key, value, category, priority, channel
572
+ FROM context_items
573
+ WHERE session_id = ? AND key GLOB ?
574
+ `);
575
+ const itemsToMove = previewStmt.all(testSessionId, keyPattern);
576
+ (0, globals_1.expect)(itemsToMove.length).toBe(0);
577
+ // Handler response
578
+ const handlerResponse = {
579
+ content: [
580
+ {
581
+ type: 'text',
582
+ text: JSON.stringify({
583
+ operation: 'reassign_channel',
584
+ dryRun: true,
585
+ keyPattern: keyPattern,
586
+ newChannel: newChannel,
587
+ itemsToMove: [],
588
+ totalItems: 0,
589
+ message: 'No items would be moved',
590
+ }, null, 2),
591
+ },
592
+ ],
593
+ };
594
+ const parsed = JSON.parse(handlerResponse.content[0].text);
595
+ (0, globals_1.expect)(parsed.totalItems).toBe(0);
596
+ (0, globals_1.expect)(parsed.message).toBeTruthy();
597
+ });
598
+ });
599
+ (0, globals_1.describe)('Handler Response Formats', () => {
600
+ (0, globals_1.beforeEach)(() => {
601
+ createTestData();
602
+ });
603
+ (0, globals_1.it)('should return detailed response for successful reassignment', () => {
604
+ const keys = ['config.database.url', 'config.cache.ttl'];
605
+ const newChannel = 'production';
606
+ const updateStmt = db.prepare(`
607
+ UPDATE context_items
608
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
609
+ WHERE session_id = ? AND key IN (${keys.map(() => '?').join(',')})
610
+ `);
611
+ const result = updateStmt.run(newChannel, testSessionId, ...keys);
612
+ const handlerResponse = {
613
+ content: [
614
+ {
615
+ type: 'text',
616
+ text: JSON.stringify({
617
+ operation: 'reassign_channel',
618
+ method: 'keys',
619
+ keys: keys,
620
+ newChannel: newChannel,
621
+ itemsUpdated: result.changes,
622
+ timestamp: new Date().toISOString(),
623
+ success: true,
624
+ }, null, 2),
625
+ },
626
+ ],
627
+ };
628
+ const parsed = JSON.parse(handlerResponse.content[0].text);
629
+ (0, globals_1.expect)(parsed.method).toBe('keys');
630
+ (0, globals_1.expect)(parsed.itemsUpdated).toBe(2);
631
+ (0, globals_1.expect)(parsed.success).toBe(true);
632
+ (0, globals_1.expect)(parsed.timestamp).toBeTruthy();
633
+ });
634
+ (0, globals_1.it)('should return summary for channel-to-channel move', () => {
635
+ const fromChannel = 'feature/auth';
636
+ const toChannel = 'release/v1.0';
637
+ // Get items before move
638
+ const itemsBefore = db
639
+ .prepare('SELECT key FROM context_items WHERE session_id = ? AND channel = ?')
640
+ .all(testSessionId, fromChannel);
641
+ const updateStmt = db.prepare(`
642
+ UPDATE context_items
643
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
644
+ WHERE session_id = ? AND channel = ?
645
+ `);
646
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
647
+ const handlerResponse = {
648
+ content: [
649
+ {
650
+ type: 'text',
651
+ text: JSON.stringify({
652
+ operation: 'reassign_channel',
653
+ method: 'channel',
654
+ fromChannel: fromChannel,
655
+ toChannel: toChannel,
656
+ itemsUpdated: result.changes,
657
+ movedKeys: itemsBefore.map((item) => item.key),
658
+ timestamp: new Date().toISOString(),
659
+ success: true,
660
+ }, null, 2),
661
+ },
662
+ ],
663
+ };
664
+ const parsed = JSON.parse(handlerResponse.content[0].text);
665
+ (0, globals_1.expect)(parsed.method).toBe('channel');
666
+ (0, globals_1.expect)(parsed.movedKeys).toHaveLength(3);
667
+ (0, globals_1.expect)(parsed.fromChannel).toBe('feature/auth');
668
+ (0, globals_1.expect)(parsed.toChannel).toBe('release/v1.0');
669
+ });
670
+ (0, globals_1.it)('should include filter details in response', () => {
671
+ const keyPattern = 'config.*';
672
+ const category = 'config';
673
+ const priorities = ['high'];
674
+ const newChannel = 'critical-config';
675
+ const placeholders = priorities.map(() => '?').join(',');
676
+ const updateStmt = db.prepare(`
677
+ UPDATE context_items
678
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
679
+ WHERE session_id = ?
680
+ AND key LIKE ?
681
+ AND category = ?
682
+ AND priority IN (${placeholders})
683
+ `);
684
+ const result = updateStmt.run(newChannel, testSessionId, keyPattern.replace(/\*/g, '%'), category, ...priorities);
685
+ const handlerResponse = {
686
+ content: [
687
+ {
688
+ type: 'text',
689
+ text: JSON.stringify({
690
+ operation: 'reassign_channel',
691
+ method: 'filtered',
692
+ filters: {
693
+ keyPattern: keyPattern,
694
+ category: category,
695
+ priorities: priorities,
696
+ },
697
+ newChannel: newChannel,
698
+ itemsUpdated: result.changes,
699
+ success: true,
700
+ }, null, 2),
701
+ },
702
+ ],
703
+ };
704
+ const parsed = JSON.parse(handlerResponse.content[0].text);
705
+ (0, globals_1.expect)(parsed.method).toBe('filtered');
706
+ (0, globals_1.expect)(parsed.filters).toEqual({
707
+ keyPattern: keyPattern,
708
+ category: category,
709
+ priorities: priorities,
710
+ });
711
+ });
712
+ });
713
+ (0, globals_1.describe)('Error Handling', () => {
714
+ (0, globals_1.beforeEach)(() => {
715
+ createTestData();
716
+ });
717
+ (0, globals_1.it)('should validate channel name', () => {
718
+ const invalidChannels = ['', ' ', null, undefined];
719
+ invalidChannels.forEach(invalidChannel => {
720
+ try {
721
+ if (!invalidChannel || !invalidChannel.trim()) {
722
+ throw new validation_1.ValidationError('Channel name cannot be empty');
723
+ }
724
+ }
725
+ catch (_error) {
726
+ (0, globals_1.expect)(_error).toBeInstanceOf(validation_1.ValidationError);
727
+ (0, globals_1.expect)(_error.message).toContain('Channel name cannot be empty');
728
+ }
729
+ });
730
+ });
731
+ (0, globals_1.it)('should handle database errors gracefully', () => {
732
+ // Close database to simulate error
733
+ dbManager.close();
734
+ try {
735
+ db.prepare('UPDATE context_items SET channel = ? WHERE session_id = ?').run('new-channel', testSessionId);
736
+ }
737
+ catch (_error) {
738
+ (0, globals_1.expect)(_error).toBeTruthy();
739
+ }
740
+ });
741
+ (0, globals_1.it)('should validate reassignment parameters', () => {
742
+ // No keys, pattern, or fromChannel provided
743
+ const args = {
744
+ toChannel: 'production',
745
+ };
746
+ try {
747
+ if (!args.keys && !args.keyPattern && !args.fromChannel) {
748
+ throw new validation_1.ValidationError('Must provide either keys array, keyPattern, or fromChannel');
749
+ }
750
+ }
751
+ catch (_error) {
752
+ (0, globals_1.expect)(_error).toBeInstanceOf(validation_1.ValidationError);
753
+ (0, globals_1.expect)(_error.message).toContain('Must provide either');
754
+ }
755
+ });
756
+ (0, globals_1.it)('should prevent reassigning to same channel', () => {
757
+ const fromChannel = 'main';
758
+ const toChannel = 'main';
759
+ try {
760
+ if (fromChannel === toChannel) {
761
+ throw new validation_1.ValidationError('Source and destination channels cannot be the same');
762
+ }
763
+ }
764
+ catch (_error) {
765
+ (0, globals_1.expect)(_error).toBeInstanceOf(validation_1.ValidationError);
766
+ (0, globals_1.expect)(_error.message).toContain('cannot be the same');
767
+ }
768
+ });
769
+ (0, globals_1.it)('should handle SQL injection in channel names', () => {
770
+ const maliciousChannel = "'; DROP TABLE context_items; --";
771
+ const keys = ['config.database.url'];
772
+ // Parameterized queries should prevent injection
773
+ const updateStmt = db.prepare(`
774
+ UPDATE context_items
775
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
776
+ WHERE session_id = ? AND key = ?
777
+ `);
778
+ const result = updateStmt.run(maliciousChannel, testSessionId, keys[0]);
779
+ // Should work normally
780
+ (0, globals_1.expect)(result.changes).toBe(1);
781
+ // Verify table still exists
782
+ const tableExists = db
783
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='context_items'")
784
+ .get();
785
+ (0, globals_1.expect)(tableExists).toBeTruthy();
786
+ // Verify the channel was set correctly
787
+ const item = db.prepare('SELECT * FROM context_items WHERE key = ?').get(keys[0]);
788
+ (0, globals_1.expect)(item.channel).toBe(maliciousChannel);
789
+ });
790
+ });
791
+ (0, globals_1.describe)('Transaction Support', () => {
792
+ (0, globals_1.beforeEach)(() => {
793
+ createTestData();
794
+ });
795
+ (0, globals_1.it)('should perform reassignment in a transaction', () => {
796
+ const fromChannel = 'feature/auth';
797
+ const toChannel = 'production';
798
+ let itemsUpdated = 0;
799
+ try {
800
+ db.prepare('BEGIN TRANSACTION').run();
801
+ // First, get the items that will be moved
802
+ const itemsToMove = db
803
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
804
+ .all(testSessionId, fromChannel);
805
+ // Update the items
806
+ const updateStmt = db.prepare(`
807
+ UPDATE context_items
808
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
809
+ WHERE session_id = ? AND channel = ?
810
+ `);
811
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
812
+ itemsUpdated = result.changes;
813
+ // Verify within transaction
814
+ const movedItems = db
815
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND channel = ?')
816
+ .all(testSessionId, toChannel);
817
+ (0, globals_1.expect)(movedItems.length).toBe(itemsToMove.length);
818
+ db.prepare('COMMIT').run();
819
+ }
820
+ catch (_error) {
821
+ db.prepare('ROLLBACK').run();
822
+ throw _error;
823
+ }
824
+ (0, globals_1.expect)(itemsUpdated).toBe(3);
825
+ });
826
+ (0, globals_1.it)('should rollback on error', () => {
827
+ const fromChannel = 'main';
828
+ const toChannel = 'production';
829
+ // Count items before transaction
830
+ const countBefore = db
831
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND channel = ?')
832
+ .get(testSessionId, fromChannel).count;
833
+ try {
834
+ db.prepare('BEGIN TRANSACTION').run();
835
+ // Start update
836
+ const updateStmt = db.prepare(`
837
+ UPDATE context_items
838
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
839
+ WHERE session_id = ? AND channel = ?
840
+ `);
841
+ updateStmt.run(toChannel, testSessionId, fromChannel);
842
+ // Simulate an error
843
+ throw new Error('Simulated error');
844
+ }
845
+ catch (_error) {
846
+ db.prepare('ROLLBACK').run();
847
+ }
848
+ // Count items after rollback
849
+ const countAfter = db
850
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND channel = ?')
851
+ .get(testSessionId, fromChannel).count;
852
+ (0, globals_1.expect)(countAfter).toBe(countBefore); // No changes
853
+ });
854
+ });
855
+ (0, globals_1.describe)('Performance and Scalability', () => {
856
+ (0, globals_1.it)('should handle large batch reassignments efficiently', () => {
857
+ // Create 1000 items
858
+ const stmt = db.prepare(`
859
+ INSERT INTO context_items (id, session_id, key, value, channel)
860
+ VALUES (?, ?, ?, ?, ?)
861
+ `);
862
+ for (let i = 0; i < 1000; i++) {
863
+ stmt.run((0, uuid_1.v4)(), testSessionId, `bulk.item.${i.toString().padStart(4, '0')}`, `Bulk value ${i}`, 'bulk-source');
864
+ }
865
+ const fromChannel = 'bulk-source';
866
+ const toChannel = 'bulk-target';
867
+ const startTime = Date.now();
868
+ const updateStmt = db.prepare(`
869
+ UPDATE context_items
870
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
871
+ WHERE session_id = ? AND channel = ?
872
+ `);
873
+ const result = updateStmt.run(toChannel, testSessionId, fromChannel);
874
+ const endTime = Date.now();
875
+ (0, globals_1.expect)(result.changes).toBe(1000);
876
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(1000); // Should complete within 1 second
877
+ // Verify all items moved
878
+ const remainingCount = db
879
+ .prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND channel = ?')
880
+ .get(testSessionId, fromChannel).count;
881
+ (0, globals_1.expect)(remainingCount).toBe(0);
882
+ });
883
+ (0, globals_1.it)('should handle pattern matching on large datasets', () => {
884
+ // Create items with various patterns
885
+ const patterns = ['config', 'feature', 'task', 'dev', 'test'];
886
+ const stmt = db.prepare(`
887
+ INSERT INTO context_items (id, session_id, key, value, channel)
888
+ VALUES (?, ?, ?, ?, ?)
889
+ `);
890
+ for (let i = 0; i < 500; i++) {
891
+ const pattern = patterns[i % patterns.length];
892
+ stmt.run((0, uuid_1.v4)(), testSessionId, `${pattern}.item.${i}`, `Value ${i}`, 'mixed-channel');
893
+ }
894
+ const keyPattern = 'config.*';
895
+ const newChannel = 'config-channel';
896
+ const startTime = Date.now();
897
+ const updateStmt = db.prepare(`
898
+ UPDATE context_items
899
+ SET channel = ?, updated_at = CURRENT_TIMESTAMP
900
+ WHERE session_id = ? AND key GLOB ?
901
+ `);
902
+ const result = updateStmt.run(newChannel, testSessionId, keyPattern);
903
+ const endTime = Date.now();
904
+ (0, globals_1.expect)(result.changes).toBe(100); // 500 / 5 patterns
905
+ (0, globals_1.expect)(endTime - startTime).toBeLessThan(500); // Should be fast
906
+ });
907
+ });
908
+ });