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.
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1051 -0
- package/bin/mcp-memory-keeper +52 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +938 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +407 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/git-integration.test.js +237 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +283 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +307 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4310 -0
- package/dist/index.phase1.backup.js +410 -0
- package/dist/index.phase2.backup.js +704 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +1873 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/server.js +384 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +731 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- 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
|
+
});
|