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,1291 @@
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 RepositoryManager_1 = require("../../repositories/RepositoryManager");
39
+ const database_test_helper_1 = require("../helpers/database-test-helper");
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const uuid_1 = require("uuid");
44
+ (0, globals_1.describe)('Channel Management Handler Integration Tests', () => {
45
+ let dbManager;
46
+ let _repositories;
47
+ let tempDbPath;
48
+ let db;
49
+ let testHelper;
50
+ let testSessionId;
51
+ let testSessionId2;
52
+ let testSessionId3;
53
+ (0, globals_1.beforeEach)(() => {
54
+ tempDbPath = path.join(os.tmpdir(), `test-channel-management-${Date.now()}.db`);
55
+ dbManager = new database_1.DatabaseManager({
56
+ filename: tempDbPath,
57
+ maxSize: 10 * 1024 * 1024,
58
+ walMode: true,
59
+ });
60
+ db = dbManager.getDatabase();
61
+ _repositories = new RepositoryManager_1.RepositoryManager(dbManager);
62
+ testHelper = new database_test_helper_1.DatabaseTestHelper(db);
63
+ // Create test sessions with different channels
64
+ testSessionId = (0, uuid_1.v4)();
65
+ testSessionId2 = (0, uuid_1.v4)();
66
+ testSessionId3 = (0, uuid_1.v4)();
67
+ // Insert test sessions
68
+ db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Dev Session', 'dev-channel');
69
+ db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId2, 'Feature Session', 'feature-auth');
70
+ db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId3, 'Production Session', 'prod-channel');
71
+ });
72
+ (0, globals_1.afterEach)(() => {
73
+ dbManager.close();
74
+ try {
75
+ fs.unlinkSync(tempDbPath);
76
+ fs.unlinkSync(`${tempDbPath}-wal`);
77
+ fs.unlinkSync(`${tempDbPath}-shm`);
78
+ }
79
+ catch (_e) {
80
+ // Ignore
81
+ }
82
+ });
83
+ (0, globals_1.describe)('context_list_channels', () => {
84
+ (0, globals_1.beforeEach)(() => {
85
+ // Create diverse test data across channels and sessions
86
+ const now = new Date();
87
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
88
+ const lastWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
89
+ // Dev channel items (mixed sessions)
90
+ const devItems = [
91
+ {
92
+ session: testSessionId,
93
+ key: 'dev1',
94
+ value: 'Dev item 1',
95
+ channel: 'dev-channel',
96
+ priority: 'high',
97
+ category: 'code',
98
+ created_at: now.toISOString(),
99
+ is_private: 0,
100
+ },
101
+ {
102
+ session: testSessionId,
103
+ key: 'dev2',
104
+ value: 'Dev item 2',
105
+ channel: 'dev-channel',
106
+ priority: 'normal',
107
+ category: 'config',
108
+ created_at: yesterday.toISOString(),
109
+ is_private: 0,
110
+ },
111
+ {
112
+ session: testSessionId2,
113
+ key: 'dev3',
114
+ value: 'Dev item 3',
115
+ channel: 'dev-channel',
116
+ priority: 'high',
117
+ category: 'code',
118
+ created_at: lastWeek.toISOString(),
119
+ is_private: 0,
120
+ },
121
+ {
122
+ session: testSessionId,
123
+ key: 'dev4',
124
+ value: 'Private dev item',
125
+ channel: 'dev-channel',
126
+ priority: 'low',
127
+ category: 'note',
128
+ created_at: now.toISOString(),
129
+ is_private: 1,
130
+ },
131
+ ];
132
+ // Feature channel items
133
+ const featureItems = [
134
+ {
135
+ session: testSessionId2,
136
+ key: 'feat1',
137
+ value: 'Feature item 1',
138
+ channel: 'feature-auth',
139
+ priority: 'high',
140
+ category: 'task',
141
+ created_at: now.toISOString(),
142
+ is_private: 0,
143
+ },
144
+ {
145
+ session: testSessionId2,
146
+ key: 'feat2',
147
+ value: 'Feature item 2',
148
+ channel: 'feature-auth',
149
+ priority: 'normal',
150
+ category: 'progress',
151
+ created_at: yesterday.toISOString(),
152
+ is_private: 0,
153
+ },
154
+ {
155
+ session: testSessionId,
156
+ key: 'feat3',
157
+ value: 'Cross-session feature',
158
+ channel: 'feature-auth',
159
+ priority: 'high',
160
+ category: 'decision',
161
+ created_at: now.toISOString(),
162
+ is_private: 0,
163
+ },
164
+ ];
165
+ // Production channel items
166
+ const prodItems = [
167
+ {
168
+ session: testSessionId3,
169
+ key: 'prod1',
170
+ value: 'Production config',
171
+ channel: 'prod-channel',
172
+ priority: 'high',
173
+ category: 'config',
174
+ created_at: now.toISOString(),
175
+ is_private: 0,
176
+ },
177
+ {
178
+ session: testSessionId3,
179
+ key: 'prod2',
180
+ value: 'Private prod data',
181
+ channel: 'prod-channel',
182
+ priority: 'high',
183
+ category: 'config',
184
+ created_at: now.toISOString(),
185
+ is_private: 1,
186
+ },
187
+ ];
188
+ // General channel items
189
+ const generalItems = [
190
+ {
191
+ session: testSessionId,
192
+ key: 'gen1',
193
+ value: 'General note',
194
+ channel: 'general',
195
+ priority: 'normal',
196
+ category: 'note',
197
+ created_at: now.toISOString(),
198
+ is_private: 0,
199
+ },
200
+ {
201
+ session: testSessionId2,
202
+ key: 'gen2',
203
+ value: 'General task',
204
+ channel: 'general',
205
+ priority: 'low',
206
+ category: 'task',
207
+ created_at: yesterday.toISOString(),
208
+ is_private: 0,
209
+ },
210
+ ];
211
+ // Empty channel for edge cases
212
+ const emptyItems = [
213
+ {
214
+ session: testSessionId,
215
+ key: 'empty1',
216
+ value: 'Item with empty channel',
217
+ channel: 'empty-channel',
218
+ priority: 'normal',
219
+ category: 'note',
220
+ created_at: lastWeek.toISOString(),
221
+ is_private: 0,
222
+ },
223
+ ];
224
+ // Insert all items
225
+ const allItems = [...devItems, ...featureItems, ...prodItems, ...generalItems, ...emptyItems];
226
+ // Disable triggers to control timestamps precisely
227
+ testHelper.disableTimestampTriggers();
228
+ const stmt = db.prepare(`
229
+ INSERT INTO context_items (id, session_id, key, value, channel, priority, category, created_at, updated_at, is_private, size)
230
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
231
+ `);
232
+ for (const item of allItems) {
233
+ stmt.run((0, uuid_1.v4)(), item.session, item.key, item.value, item.channel, item.priority, item.category, item.created_at, item.created_at, // Set updated_at to match created_at
234
+ item.is_private, item.value.length // size
235
+ );
236
+ }
237
+ // Re-enable triggers
238
+ testHelper.enableTimestampTriggers();
239
+ });
240
+ (0, globals_1.describe)('Basic listing functionality', () => {
241
+ (0, globals_1.it)('should list all channels with counts across all sessions', () => {
242
+ // Simulate handler logic for listing all channels
243
+ const sql = `
244
+ SELECT
245
+ channel,
246
+ COUNT(*) as total_count,
247
+ COUNT(DISTINCT session_id) as session_count,
248
+ SUM(CASE WHEN is_private = 0 THEN 1 ELSE 0 END) as public_count,
249
+ SUM(CASE WHEN is_private = 1 THEN 1 ELSE 0 END) as private_count
250
+ FROM context_items
251
+ GROUP BY channel
252
+ ORDER BY total_count DESC, channel ASC
253
+ `;
254
+ const channels = db.prepare(sql).all();
255
+ (0, globals_1.expect)(channels).toHaveLength(5); // dev-channel, feature-auth, prod-channel, general, empty-channel
256
+ // Verify dev-channel (most items)
257
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
258
+ (0, globals_1.expect)(devChannel).toBeDefined();
259
+ (0, globals_1.expect)(devChannel.total_count).toBe(4);
260
+ (0, globals_1.expect)(devChannel.session_count).toBe(2); // Used by session 1 and 2
261
+ (0, globals_1.expect)(devChannel.public_count).toBe(3);
262
+ (0, globals_1.expect)(devChannel.private_count).toBe(1);
263
+ // Verify feature-auth
264
+ const featureChannel = channels.find((c) => c.channel === 'feature-auth');
265
+ (0, globals_1.expect)(featureChannel).toBeDefined();
266
+ (0, globals_1.expect)(featureChannel.total_count).toBe(3);
267
+ (0, globals_1.expect)(featureChannel.session_count).toBe(2);
268
+ (0, globals_1.expect)(featureChannel.public_count).toBe(3);
269
+ (0, globals_1.expect)(featureChannel.private_count).toBe(0);
270
+ // Verify sorting by count
271
+ (0, globals_1.expect)(channels[0].channel).toBe('dev-channel'); // 4 items
272
+ (0, globals_1.expect)(channels[1].channel).toBe('feature-auth'); // 3 items
273
+ });
274
+ (0, globals_1.it)('should filter channels by specific session', () => {
275
+ const sql = `
276
+ SELECT
277
+ channel,
278
+ COUNT(*) as total_count,
279
+ SUM(CASE WHEN is_private = 0 THEN 1 ELSE 0 END) as public_count,
280
+ SUM(CASE WHEN is_private = 1 THEN 1 ELSE 0 END) as private_count,
281
+ MAX(created_at) as last_activity,
282
+ GROUP_CONCAT(DISTINCT category) as categories
283
+ FROM context_items
284
+ WHERE session_id = ?
285
+ GROUP BY channel
286
+ ORDER BY total_count DESC
287
+ `;
288
+ const channels = db.prepare(sql).all(testSessionId);
289
+ (0, globals_1.expect)(channels).toHaveLength(4); // dev-channel, feature-auth, general, empty-channel
290
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
291
+ (0, globals_1.expect)(devChannel.total_count).toBe(3); // Only items from testSessionId (dev1, dev2, dev4)
292
+ (0, globals_1.expect)(devChannel.public_count).toBe(2); // dev1, dev2
293
+ (0, globals_1.expect)(devChannel.private_count).toBe(1); // dev4
294
+ (0, globals_1.expect)(devChannel.categories).toContain('code');
295
+ (0, globals_1.expect)(devChannel.categories).toContain('config');
296
+ });
297
+ (0, globals_1.it)('should filter channels by multiple sessions', () => {
298
+ const sessions = [testSessionId, testSessionId2];
299
+ const placeholders = sessions.map(() => '?').join(',');
300
+ const sql = `
301
+ SELECT
302
+ channel,
303
+ COUNT(*) as total_count,
304
+ COUNT(DISTINCT session_id) as session_count,
305
+ GROUP_CONCAT(DISTINCT session_id) as session_ids
306
+ FROM context_items
307
+ WHERE session_id IN (${placeholders})
308
+ GROUP BY channel
309
+ ORDER BY channel ASC
310
+ `;
311
+ const channels = db.prepare(sql).all(...sessions);
312
+ (0, globals_1.expect)(channels).toHaveLength(4); // dev-channel, feature-auth, general, empty-channel
313
+ // Verify dev-channel is used by both sessions
314
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
315
+ (0, globals_1.expect)(devChannel.session_count).toBe(2);
316
+ (0, globals_1.expect)(devChannel.session_ids).toContain(testSessionId);
317
+ (0, globals_1.expect)(devChannel.session_ids).toContain(testSessionId2);
318
+ // Verify prod-channel is NOT included (session3 not in filter)
319
+ const prodChannel = channels.find((c) => c.channel === 'prod-channel');
320
+ (0, globals_1.expect)(prodChannel).toBeUndefined();
321
+ });
322
+ });
323
+ (0, globals_1.describe)('Sort options', () => {
324
+ (0, globals_1.it)('should sort channels by name alphabetically', () => {
325
+ const sql = `
326
+ SELECT channel, COUNT(*) as total_count
327
+ FROM context_items
328
+ GROUP BY channel
329
+ ORDER BY channel ASC
330
+ `;
331
+ const channels = db.prepare(sql).all();
332
+ const channelNames = channels.map((c) => c.channel);
333
+ (0, globals_1.expect)(channelNames).toEqual([
334
+ 'dev-channel',
335
+ 'empty-channel',
336
+ 'feature-auth',
337
+ 'general',
338
+ 'prod-channel',
339
+ ]);
340
+ });
341
+ (0, globals_1.it)('should sort channels by count descending', () => {
342
+ const sql = `
343
+ SELECT channel, COUNT(*) as total_count
344
+ FROM context_items
345
+ GROUP BY channel
346
+ ORDER BY total_count DESC, channel ASC
347
+ `;
348
+ const channels = db.prepare(sql).all();
349
+ (0, globals_1.expect)(channels[0].channel).toBe('dev-channel'); // 4 items
350
+ (0, globals_1.expect)(channels[0].total_count).toBe(4);
351
+ (0, globals_1.expect)(channels[1].channel).toBe('feature-auth'); // 3 items
352
+ (0, globals_1.expect)(channels[1].total_count).toBe(3);
353
+ (0, globals_1.expect)(channels[channels.length - 1].channel).toBe('empty-channel'); // 1 item
354
+ (0, globals_1.expect)(channels[channels.length - 1].total_count).toBe(1);
355
+ });
356
+ (0, globals_1.it)('should sort channels by last activity', () => {
357
+ const sql = `
358
+ SELECT
359
+ channel,
360
+ MAX(created_at) as last_activity,
361
+ COUNT(*) as total_count
362
+ FROM context_items
363
+ GROUP BY channel
364
+ ORDER BY last_activity DESC
365
+ `;
366
+ const channels = db.prepare(sql).all();
367
+ // Channels with recent activity should be first
368
+ const topChannels = channels.slice(0, 4).map((c) => c.channel);
369
+ (0, globals_1.expect)(topChannels).toContain('dev-channel'); // Has items from "now"
370
+ (0, globals_1.expect)(topChannels).toContain('feature-auth'); // Has items from "now"
371
+ (0, globals_1.expect)(topChannels).toContain('prod-channel'); // Has items from "now"
372
+ (0, globals_1.expect)(topChannels).toContain('general'); // Has items from "now"
373
+ // Empty channel should be last (only has old items)
374
+ (0, globals_1.expect)(channels[channels.length - 1].channel).toBe('empty-channel');
375
+ });
376
+ });
377
+ (0, globals_1.describe)('Privacy boundaries', () => {
378
+ (0, globals_1.it)('should respect privacy when listing channels for a session', () => {
379
+ // When querying as testSessionId, should see own private items
380
+ const sql = `
381
+ SELECT
382
+ channel,
383
+ COUNT(*) as total_count,
384
+ SUM(CASE WHEN is_private = 1 THEN 1 ELSE 0 END) as private_count
385
+ FROM context_items
386
+ WHERE session_id = ? OR is_private = 0
387
+ GROUP BY channel
388
+ `;
389
+ const channelsAsSession1 = db.prepare(sql).all(testSessionId);
390
+ // Find dev-channel stats
391
+ const devChannel = channelsAsSession1.find((c) => c.channel === 'dev-channel');
392
+ (0, globals_1.expect)(devChannel.total_count).toBe(4); // Can see all including own private
393
+ (0, globals_1.expect)(devChannel.private_count).toBe(1);
394
+ // Find prod-channel stats
395
+ const prodChannel = channelsAsSession1.find((c) => c.channel === 'prod-channel');
396
+ (0, globals_1.expect)(prodChannel.total_count).toBe(1); // Can only see public items
397
+ (0, globals_1.expect)(prodChannel.private_count).toBe(0); // Cannot see session3's private items
398
+ });
399
+ (0, globals_1.it)('should show different counts based on viewing session', () => {
400
+ // Compare prod-channel visibility from different sessions
401
+ const sqlForSession = `
402
+ SELECT
403
+ channel,
404
+ COUNT(*) as visible_count
405
+ FROM context_items
406
+ WHERE channel = 'prod-channel'
407
+ AND (session_id = ? OR is_private = 0)
408
+ `;
409
+ // As session3 (owns prod items)
410
+ const resultAsOwner = db.prepare(sqlForSession).get(testSessionId3);
411
+ (0, globals_1.expect)(resultAsOwner.visible_count).toBe(2); // Sees both public and private
412
+ // As session1 (different session)
413
+ const resultAsOther = db.prepare(sqlForSession).get(testSessionId);
414
+ (0, globals_1.expect)(resultAsOther.visible_count).toBe(1); // Only sees public
415
+ });
416
+ (0, globals_1.it)('should include privacy breakdown in channel list', () => {
417
+ const sql = `
418
+ SELECT
419
+ channel,
420
+ COUNT(*) as total_count,
421
+ SUM(CASE WHEN is_private = 0 THEN 1 ELSE 0 END) as public_count,
422
+ SUM(CASE WHEN is_private = 1 AND session_id = ? THEN 1 ELSE 0 END) as own_private_count,
423
+ SUM(CASE WHEN is_private = 1 AND session_id != ? THEN 1 ELSE 0 END) as other_private_count
424
+ FROM context_items
425
+ GROUP BY channel
426
+ `;
427
+ const channels = db.prepare(sql).all(testSessionId, testSessionId);
428
+ // Check dev-channel privacy breakdown
429
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
430
+ (0, globals_1.expect)(devChannel.public_count).toBe(3);
431
+ (0, globals_1.expect)(devChannel.own_private_count).toBe(1); // testSessionId owns 1 private
432
+ (0, globals_1.expect)(devChannel.other_private_count).toBe(0);
433
+ // Check prod-channel privacy breakdown
434
+ const prodChannel = channels.find((c) => c.channel === 'prod-channel');
435
+ (0, globals_1.expect)(prodChannel.public_count).toBe(1);
436
+ (0, globals_1.expect)(prodChannel.own_private_count).toBe(0); // testSessionId owns no private
437
+ (0, globals_1.expect)(prodChannel.other_private_count).toBe(1); // testSessionId3 owns 1 private
438
+ });
439
+ });
440
+ (0, globals_1.describe)('Empty results handling', () => {
441
+ (0, globals_1.it)('should return empty array when no channels exist', () => {
442
+ // Clear all data
443
+ db.prepare('DELETE FROM context_items').run();
444
+ const sql = 'SELECT channel FROM context_items GROUP BY channel';
445
+ const channels = db.prepare(sql).all();
446
+ (0, globals_1.expect)(channels).toEqual([]);
447
+ });
448
+ (0, globals_1.it)('should return empty array when filtering by non-existent session', () => {
449
+ const sql = `
450
+ SELECT channel, COUNT(*) as count
451
+ FROM context_items
452
+ WHERE session_id = ?
453
+ GROUP BY channel
454
+ `;
455
+ const channels = db.prepare(sql).all('non-existent-session');
456
+ (0, globals_1.expect)(channels).toEqual([]);
457
+ });
458
+ (0, globals_1.it)('should handle channels with zero visible items gracefully', () => {
459
+ // Create a session that can't see any items in certain channels
460
+ const newSessionId = (0, uuid_1.v4)();
461
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(newSessionId, 'New Session');
462
+ // All prod-channel items are either private to session3 or we make the public one private too
463
+ db.prepare('UPDATE context_items SET is_private = 1 WHERE channel = ?').run('prod-channel');
464
+ const sql = `
465
+ SELECT
466
+ c.channel,
467
+ COUNT(ci.id) as visible_count
468
+ FROM (SELECT DISTINCT channel FROM context_items) c
469
+ LEFT JOIN context_items ci ON c.channel = ci.channel
470
+ AND (ci.session_id = ? OR ci.is_private = 0)
471
+ GROUP BY c.channel
472
+ ORDER BY c.channel
473
+ `;
474
+ const channels = db.prepare(sql).all(newSessionId);
475
+ // Should still list prod-channel but with 0 visible items
476
+ const prodChannel = channels.find((c) => c.channel === 'prod-channel');
477
+ (0, globals_1.expect)(prodChannel).toBeDefined();
478
+ (0, globals_1.expect)(prodChannel.visible_count).toBe(0);
479
+ });
480
+ });
481
+ (0, globals_1.describe)('Additional metadata', () => {
482
+ (0, globals_1.it)('should include category distribution per channel', () => {
483
+ const sql = `
484
+ SELECT
485
+ channel,
486
+ COUNT(*) as total_count,
487
+ GROUP_CONCAT(DISTINCT category) as categories,
488
+ COUNT(DISTINCT category) as category_count
489
+ FROM context_items
490
+ GROUP BY channel
491
+ `;
492
+ const channels = db.prepare(sql).all();
493
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
494
+ (0, globals_1.expect)(devChannel.categories).toContain('code');
495
+ (0, globals_1.expect)(devChannel.categories).toContain('config');
496
+ (0, globals_1.expect)(devChannel.categories).toContain('note');
497
+ (0, globals_1.expect)(devChannel.category_count).toBe(3);
498
+ });
499
+ (0, globals_1.it)('should include priority distribution per channel', () => {
500
+ const sql = `
501
+ SELECT
502
+ channel,
503
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high_priority,
504
+ SUM(CASE WHEN priority = 'normal' THEN 1 ELSE 0 END) as normal_priority,
505
+ SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low_priority
506
+ FROM context_items
507
+ GROUP BY channel
508
+ `;
509
+ const channels = db.prepare(sql).all();
510
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
511
+ (0, globals_1.expect)(devChannel.high_priority).toBe(2);
512
+ (0, globals_1.expect)(devChannel.normal_priority).toBe(1);
513
+ (0, globals_1.expect)(devChannel.low_priority).toBe(1);
514
+ });
515
+ (0, globals_1.it)('should include time-based activity metrics', () => {
516
+ const sql = `
517
+ SELECT
518
+ channel,
519
+ MIN(created_at) as first_activity,
520
+ MAX(created_at) as last_activity,
521
+ julianday(MAX(created_at)) - julianday(MIN(created_at)) as days_active
522
+ FROM context_items
523
+ GROUP BY channel
524
+ `;
525
+ const channels = db.prepare(sql).all();
526
+ const devChannel = channels.find((c) => c.channel === 'dev-channel');
527
+ (0, globals_1.expect)(devChannel.first_activity).toBeDefined();
528
+ (0, globals_1.expect)(devChannel.last_activity).toBeDefined();
529
+ (0, globals_1.expect)(devChannel.days_active).toBeGreaterThanOrEqual(0);
530
+ });
531
+ });
532
+ });
533
+ (0, globals_1.describe)('context_channel_stats', () => {
534
+ (0, globals_1.beforeEach)(() => {
535
+ // Create comprehensive test data for statistics
536
+ const now = new Date();
537
+ const hourAgo = new Date(now.getTime() - 60 * 60 * 1000);
538
+ const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
539
+ const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
540
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
541
+ // Create items with varied timestamps and updates
542
+ const items = [
543
+ // Dev channel - high activity
544
+ {
545
+ session: testSessionId,
546
+ key: 'dev_task_1',
547
+ value: 'Implement auth',
548
+ channel: 'dev-channel',
549
+ priority: 'high',
550
+ category: 'task',
551
+ created_at: weekAgo,
552
+ updated_at: hourAgo,
553
+ },
554
+ {
555
+ session: testSessionId,
556
+ key: 'dev_bug_1',
557
+ value: 'Fix login bug',
558
+ channel: 'dev-channel',
559
+ priority: 'high',
560
+ category: 'error',
561
+ created_at: twoDaysAgo,
562
+ updated_at: yesterday,
563
+ },
564
+ {
565
+ session: testSessionId,
566
+ key: 'dev_note_1',
567
+ value: 'API docs',
568
+ channel: 'dev-channel',
569
+ priority: 'normal',
570
+ category: 'note',
571
+ created_at: yesterday,
572
+ updated_at: yesterday,
573
+ },
574
+ {
575
+ session: testSessionId2,
576
+ key: 'dev_progress_1',
577
+ value: '50% complete',
578
+ channel: 'dev-channel',
579
+ priority: 'normal',
580
+ category: 'progress',
581
+ created_at: hourAgo,
582
+ updated_at: hourAgo,
583
+ },
584
+ {
585
+ session: testSessionId,
586
+ key: 'dev_decision_1',
587
+ value: 'Use JWT',
588
+ channel: 'dev-channel',
589
+ priority: 'high',
590
+ category: 'decision',
591
+ created_at: twoDaysAgo,
592
+ updated_at: twoDaysAgo,
593
+ is_private: 1,
594
+ },
595
+ // Feature channel - moderate activity
596
+ {
597
+ session: testSessionId2,
598
+ key: 'feat_task_1',
599
+ value: 'Design UI',
600
+ channel: 'feature-auth',
601
+ priority: 'high',
602
+ category: 'task',
603
+ created_at: twoDaysAgo,
604
+ updated_at: hourAgo,
605
+ },
606
+ {
607
+ session: testSessionId2,
608
+ key: 'feat_task_2',
609
+ value: 'Write tests',
610
+ channel: 'feature-auth',
611
+ priority: 'normal',
612
+ category: 'task',
613
+ created_at: yesterday,
614
+ updated_at: yesterday,
615
+ },
616
+ {
617
+ session: testSessionId,
618
+ key: 'feat_warning_1',
619
+ value: 'Deprecation notice',
620
+ channel: 'feature-auth',
621
+ priority: 'high',
622
+ category: 'warning',
623
+ created_at: hourAgo,
624
+ updated_at: hourAgo,
625
+ },
626
+ // General channel - low activity
627
+ {
628
+ session: testSessionId,
629
+ key: 'gen_note_1',
630
+ value: 'Meeting notes',
631
+ channel: 'general',
632
+ priority: 'low',
633
+ category: 'note',
634
+ created_at: weekAgo,
635
+ updated_at: weekAgo,
636
+ },
637
+ {
638
+ session: testSessionId3,
639
+ key: 'gen_task_1',
640
+ value: 'Review PR',
641
+ channel: 'general',
642
+ priority: 'normal',
643
+ category: 'task',
644
+ created_at: yesterday,
645
+ updated_at: yesterday,
646
+ },
647
+ ];
648
+ // Disable triggers to control timestamps precisely
649
+ testHelper.disableTimestampTriggers();
650
+ const stmt = db.prepare(`
651
+ INSERT INTO context_items (
652
+ id, session_id, key, value, channel, priority, category,
653
+ created_at, updated_at, is_private, size
654
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
655
+ `);
656
+ for (const item of items) {
657
+ stmt.run((0, uuid_1.v4)(), item.session, item.key, item.value, item.channel, item.priority, item.category, item.created_at.toISOString(), item.updated_at.toISOString(), item.is_private || 0, Buffer.byteLength(item.value, 'utf8'));
658
+ }
659
+ // Re-enable triggers
660
+ testHelper.enableTimestampTriggers();
661
+ });
662
+ (0, globals_1.describe)('Single channel statistics', () => {
663
+ (0, globals_1.it)('should return detailed stats for a specific channel', () => {
664
+ const channel = 'dev-channel';
665
+ // Basic stats
666
+ const basicStats = db
667
+ .prepare(`
668
+ SELECT
669
+ COUNT(*) as total_items,
670
+ COUNT(DISTINCT session_id) as unique_sessions,
671
+ COUNT(DISTINCT category) as unique_categories,
672
+ SUM(size) as total_size,
673
+ AVG(size) as avg_size,
674
+ MIN(created_at) as first_activity,
675
+ MAX(created_at) as last_activity,
676
+ MAX(updated_at) as last_update
677
+ FROM context_items
678
+ WHERE channel = ?
679
+ `)
680
+ .get(channel);
681
+ (0, globals_1.expect)(basicStats.total_items).toBe(5);
682
+ (0, globals_1.expect)(basicStats.unique_sessions).toBe(2);
683
+ (0, globals_1.expect)(basicStats.unique_categories).toBe(5); // task, error, note, progress, decision
684
+ (0, globals_1.expect)(basicStats.total_size).toBeGreaterThan(0);
685
+ (0, globals_1.expect)(basicStats.avg_size).toBeGreaterThan(0);
686
+ (0, globals_1.expect)(basicStats.first_activity).toBeDefined();
687
+ (0, globals_1.expect)(basicStats.last_activity).toBeDefined();
688
+ });
689
+ (0, globals_1.it)('should calculate category distribution for a channel', () => {
690
+ const categoryStats = db
691
+ .prepare(`
692
+ SELECT
693
+ category,
694
+ COUNT(*) as count,
695
+ ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM context_items WHERE channel = ?), 2) as percentage
696
+ FROM context_items
697
+ WHERE channel = ?
698
+ GROUP BY category
699
+ ORDER BY count DESC
700
+ `)
701
+ .all('dev-channel', 'dev-channel');
702
+ (0, globals_1.expect)(categoryStats).toHaveLength(5);
703
+ // Verify percentages add up to 100
704
+ const totalPercentage = categoryStats.reduce((sum, cat) => sum + cat.percentage, 0);
705
+ (0, globals_1.expect)(totalPercentage).toBeCloseTo(100, 1);
706
+ // Each category should have exactly 1 item (20% each)
707
+ categoryStats.forEach((cat) => {
708
+ (0, globals_1.expect)(cat.count).toBe(1);
709
+ (0, globals_1.expect)(cat.percentage).toBe(20);
710
+ });
711
+ });
712
+ (0, globals_1.it)('should calculate priority distribution for a channel', () => {
713
+ const priorityStats = db
714
+ .prepare(`
715
+ SELECT
716
+ priority,
717
+ COUNT(*) as count,
718
+ ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM context_items WHERE channel = ?), 2) as percentage
719
+ FROM context_items
720
+ WHERE channel = ?
721
+ GROUP BY priority
722
+ ORDER BY
723
+ CASE priority
724
+ WHEN 'high' THEN 1
725
+ WHEN 'normal' THEN 2
726
+ WHEN 'low' THEN 3
727
+ END
728
+ `)
729
+ .all('dev-channel', 'dev-channel');
730
+ (0, globals_1.expect)(priorityStats).toHaveLength(2); // high and normal only in dev-channel
731
+ const highPriority = priorityStats.find((p) => p.priority === 'high');
732
+ (0, globals_1.expect)(highPriority.count).toBe(3); // 60%
733
+ (0, globals_1.expect)(highPriority.percentage).toBe(60);
734
+ const normalPriority = priorityStats.find((p) => p.priority === 'normal');
735
+ (0, globals_1.expect)(normalPriority.count).toBe(2); // 40%
736
+ (0, globals_1.expect)(normalPriority.percentage).toBe(40);
737
+ });
738
+ (0, globals_1.it)('should calculate activity metrics over time', () => {
739
+ // Disable triggers to have precise control over timestamps
740
+ testHelper.disableTimestampTriggers();
741
+ const now = new Date();
742
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
743
+ const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
744
+ const activityStats = db
745
+ .prepare(`
746
+ SELECT
747
+ SUM(CASE WHEN created_at > ? THEN 1 ELSE 0 END) as items_last_24h,
748
+ SUM(CASE WHEN updated_at > ? THEN 1 ELSE 0 END) as updates_last_24h,
749
+ SUM(CASE WHEN created_at > ? THEN 1 ELSE 0 END) as items_last_week,
750
+ SUM(CASE WHEN updated_at > ? THEN 1 ELSE 0 END) as updates_last_week
751
+ FROM context_items
752
+ WHERE channel = ?
753
+ `)
754
+ .get(oneDayAgo.toISOString(), oneDayAgo.toISOString(), oneWeekAgo.toISOString(), oneWeekAgo.toISOString(), 'dev-channel');
755
+ // Re-enable triggers
756
+ testHelper.enableTimestampTriggers();
757
+ (0, globals_1.expect)(activityStats.items_last_24h).toBe(1); // Only dev_progress_1
758
+ (0, globals_1.expect)(activityStats.updates_last_24h).toBe(2); // dev_task_1 and dev_progress_1
759
+ (0, globals_1.expect)(activityStats.items_last_week).toBe(4); // All items except dev_task_1 (created exactly 7 days ago)
760
+ (0, globals_1.expect)(activityStats.updates_last_week).toBe(5); // All have been updated
761
+ });
762
+ (0, globals_1.it)('should identify top contributors (sessions) to a channel', () => {
763
+ const contributorStats = db
764
+ .prepare(`
765
+ SELECT
766
+ s.id as session_id,
767
+ s.name as session_name,
768
+ COUNT(ci.id) as item_count,
769
+ SUM(ci.size) as total_size,
770
+ MAX(ci.created_at) as last_contribution
771
+ FROM sessions s
772
+ JOIN context_items ci ON s.id = ci.session_id
773
+ WHERE ci.channel = ?
774
+ GROUP BY s.id, s.name
775
+ ORDER BY item_count DESC
776
+ `)
777
+ .all('dev-channel');
778
+ (0, globals_1.expect)(contributorStats).toHaveLength(2);
779
+ // testSessionId should be top contributor (4 items)
780
+ (0, globals_1.expect)(contributorStats[0].session_id).toBe(testSessionId);
781
+ (0, globals_1.expect)(contributorStats[0].item_count).toBe(4);
782
+ (0, globals_1.expect)(contributorStats[0].session_name).toBe('Dev Session');
783
+ // testSessionId2 should have 1 item
784
+ (0, globals_1.expect)(contributorStats[1].session_id).toBe(testSessionId2);
785
+ (0, globals_1.expect)(contributorStats[1].item_count).toBe(1);
786
+ });
787
+ });
788
+ (0, globals_1.describe)('All channels overview statistics', () => {
789
+ (0, globals_1.it)('should return aggregated stats for all channels', () => {
790
+ const overviewStats = db
791
+ .prepare(`
792
+ SELECT
793
+ COUNT(DISTINCT channel) as total_channels,
794
+ COUNT(*) as total_items,
795
+ COUNT(DISTINCT session_id) as total_sessions,
796
+ SUM(size) as total_size,
797
+ COUNT(DISTINCT category) as total_categories
798
+ FROM context_items
799
+ `)
800
+ .get();
801
+ (0, globals_1.expect)(overviewStats.total_channels).toBe(3); // dev-channel, feature-auth, general
802
+ (0, globals_1.expect)(overviewStats.total_items).toBe(10);
803
+ (0, globals_1.expect)(overviewStats.total_sessions).toBe(3);
804
+ (0, globals_1.expect)(overviewStats.total_categories).toBe(6); // task, error, note, progress, decision, warning
805
+ });
806
+ (0, globals_1.it)('should rank channels by various metrics', () => {
807
+ // Rank by item count
808
+ const byItemCount = db
809
+ .prepare(`
810
+ SELECT
811
+ channel,
812
+ COUNT(*) as item_count,
813
+ RANK() OVER (ORDER BY COUNT(*) DESC) as rank
814
+ FROM context_items
815
+ GROUP BY channel
816
+ ORDER BY rank
817
+ `)
818
+ .all();
819
+ (0, globals_1.expect)(byItemCount[0].channel).toBe('dev-channel');
820
+ (0, globals_1.expect)(byItemCount[0].item_count).toBe(5);
821
+ (0, globals_1.expect)(byItemCount[0].rank).toBe(1);
822
+ // Rank by recent activity
823
+ const byRecentActivity = db
824
+ .prepare(`
825
+ SELECT
826
+ channel,
827
+ MAX(updated_at) as last_activity,
828
+ RANK() OVER (ORDER BY MAX(updated_at) DESC) as rank
829
+ FROM context_items
830
+ GROUP BY channel
831
+ ORDER BY rank
832
+ `)
833
+ .all();
834
+ // Should be ordered by most recent activity
835
+ (0, globals_1.expect)(byRecentActivity[0].rank).toBe(1);
836
+ (0, globals_1.expect)(byRecentActivity[byRecentActivity.length - 1].channel).toBe('general'); // Least recent
837
+ });
838
+ (0, globals_1.it)('should calculate channel health metrics', () => {
839
+ // Disable triggers to ensure consistent timestamp behavior
840
+ testHelper.disableTimestampTriggers();
841
+ const now = new Date();
842
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
843
+ const _oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
844
+ const healthMetrics = db
845
+ .prepare(`
846
+ SELECT
847
+ channel,
848
+ COUNT(*) as total_items,
849
+ SUM(CASE WHEN updated_at > ? THEN 1 ELSE 0 END) as recent_updates,
850
+ COUNT(DISTINCT session_id) as active_sessions,
851
+ ROUND(
852
+ SUM(CASE WHEN updated_at > ? THEN 1 ELSE 0 END) * 100.0 / COUNT(*),
853
+ 2
854
+ ) as freshness_score,
855
+ julianday('now') - julianday(MAX(updated_at)) as days_since_update
856
+ FROM context_items
857
+ GROUP BY channel
858
+ ORDER BY freshness_score DESC
859
+ `)
860
+ .all(oneDayAgo.toISOString(), oneDayAgo.toISOString());
861
+ // Re-enable triggers
862
+ testHelper.enableTimestampTriggers();
863
+ (0, globals_1.expect)(healthMetrics).toHaveLength(3);
864
+ // Dev channel should have highest freshness (most recent updates)
865
+ const devHealth = healthMetrics.find((h) => h.channel === 'dev-channel');
866
+ (0, globals_1.expect)(devHealth.recent_updates).toBeGreaterThan(0);
867
+ (0, globals_1.expect)(devHealth.freshness_score).toBeGreaterThan(0);
868
+ (0, globals_1.expect)(devHealth.active_sessions).toBe(2);
869
+ // General channel should have lowest freshness
870
+ const generalHealth = healthMetrics.find((h) => h.channel === 'general');
871
+ (0, globals_1.expect)(generalHealth.recent_updates).toBe(0);
872
+ (0, globals_1.expect)(generalHealth.freshness_score).toBe(0);
873
+ });
874
+ });
875
+ (0, globals_1.describe)('Time-based analysis', () => {
876
+ (0, globals_1.it)('should generate hourly activity heatmap for a channel', () => {
877
+ const hourlyStats = db
878
+ .prepare(`
879
+ SELECT
880
+ strftime('%H', created_at) as hour,
881
+ COUNT(*) as items_created,
882
+ COUNT(DISTINCT session_id) as unique_sessions
883
+ FROM context_items
884
+ WHERE channel = ?
885
+ GROUP BY hour
886
+ ORDER BY hour
887
+ `)
888
+ .all('dev-channel');
889
+ // Should have entries for hours when items were created
890
+ (0, globals_1.expect)(hourlyStats.length).toBeGreaterThan(0);
891
+ hourlyStats.forEach((stat) => {
892
+ (0, globals_1.expect)(parseInt(stat.hour)).toBeGreaterThanOrEqual(0);
893
+ (0, globals_1.expect)(parseInt(stat.hour)).toBeLessThanOrEqual(23);
894
+ (0, globals_1.expect)(stat.items_created).toBeGreaterThan(0);
895
+ });
896
+ });
897
+ (0, globals_1.it)('should show daily activity trends', () => {
898
+ const dailyTrends = db
899
+ .prepare(`
900
+ SELECT
901
+ DATE(created_at) as date,
902
+ channel,
903
+ COUNT(*) as items_created,
904
+ COUNT(DISTINCT category) as categories_used
905
+ FROM context_items
906
+ WHERE created_at > date('now', '-7 days')
907
+ GROUP BY date, channel
908
+ ORDER BY date DESC, channel
909
+ `)
910
+ .all();
911
+ (0, globals_1.expect)(dailyTrends.length).toBeGreaterThan(0);
912
+ // Verify structure
913
+ dailyTrends.forEach((trend) => {
914
+ (0, globals_1.expect)(trend.date).toMatch(/^\d{4}-\d{2}-\d{2}$/);
915
+ (0, globals_1.expect)(trend.items_created).toBeGreaterThan(0);
916
+ (0, globals_1.expect)(trend.categories_used).toBeGreaterThan(0);
917
+ });
918
+ });
919
+ (0, globals_1.it)('should calculate growth rate over time periods', () => {
920
+ const now = new Date();
921
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
922
+ const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000);
923
+ const growthStats = db
924
+ .prepare(`
925
+ SELECT
926
+ channel,
927
+ SUM(CASE WHEN created_at > ? THEN 1 ELSE 0 END) as items_last_24h,
928
+ SUM(CASE WHEN created_at BETWEEN ? AND ? THEN 1 ELSE 0 END) as items_previous_24h,
929
+ CASE
930
+ WHEN SUM(CASE WHEN created_at BETWEEN ? AND ? THEN 1 ELSE 0 END) = 0 THEN 100
931
+ ELSE ROUND(
932
+ (SUM(CASE WHEN created_at > ? THEN 1 ELSE 0 END) -
933
+ SUM(CASE WHEN created_at BETWEEN ? AND ? THEN 1 ELSE 0 END)) * 100.0 /
934
+ SUM(CASE WHEN created_at BETWEEN ? AND ? THEN 1 ELSE 0 END),
935
+ 2
936
+ )
937
+ END as growth_rate
938
+ FROM context_items
939
+ GROUP BY channel
940
+ `)
941
+ .all(oneDayAgo.toISOString(), twoDaysAgo.toISOString(), oneDayAgo.toISOString(), twoDaysAgo.toISOString(), oneDayAgo.toISOString(), oneDayAgo.toISOString(), twoDaysAgo.toISOString(), oneDayAgo.toISOString(), twoDaysAgo.toISOString(), oneDayAgo.toISOString());
942
+ (0, globals_1.expect)(growthStats).toHaveLength(3);
943
+ growthStats.forEach((stat) => {
944
+ (0, globals_1.expect)(stat).toHaveProperty('items_last_24h');
945
+ (0, globals_1.expect)(stat).toHaveProperty('items_previous_24h');
946
+ (0, globals_1.expect)(stat).toHaveProperty('growth_rate');
947
+ });
948
+ });
949
+ });
950
+ (0, globals_1.describe)('Pattern detection and insights', () => {
951
+ (0, globals_1.it)('should identify most frequently updated items', () => {
952
+ const frequentUpdates = db
953
+ .prepare(`
954
+ SELECT
955
+ channel,
956
+ key,
957
+ value,
958
+ julianday(updated_at) - julianday(created_at) as days_between_create_update,
959
+ CASE
960
+ WHEN julianday(updated_at) - julianday(created_at) > 0 THEN 'frequently_updated'
961
+ ELSE 'stable'
962
+ END as update_pattern
963
+ FROM context_items
964
+ WHERE channel = ?
965
+ ORDER BY days_between_create_update DESC
966
+ `)
967
+ .all('dev-channel');
968
+ // Find items that have been updated after creation
969
+ const updatedItems = frequentUpdates.filter((item) => item.update_pattern === 'frequently_updated');
970
+ (0, globals_1.expect)(updatedItems.length).toBeGreaterThan(0);
971
+ // dev_task_1 should be most updated (created week ago, updated hour ago)
972
+ (0, globals_1.expect)(updatedItems[0].key).toBe('dev_task_1');
973
+ });
974
+ (0, globals_1.it)('should detect category usage patterns', () => {
975
+ const categoryPatterns = db
976
+ .prepare(`
977
+ WITH category_stats AS (
978
+ SELECT
979
+ channel,
980
+ category,
981
+ COUNT(*) as usage_count,
982
+ AVG(CASE priority
983
+ WHEN 'high' THEN 3
984
+ WHEN 'normal' THEN 2
985
+ WHEN 'low' THEN 1
986
+ END) as avg_priority_score
987
+ FROM context_items
988
+ GROUP BY channel, category
989
+ )
990
+ SELECT
991
+ channel,
992
+ category,
993
+ usage_count,
994
+ ROUND(avg_priority_score, 2) as avg_priority_score,
995
+ CASE
996
+ WHEN usage_count > 1 THEN 'frequent'
997
+ ELSE 'occasional'
998
+ END as usage_pattern
999
+ FROM category_stats
1000
+ ORDER BY channel, usage_count DESC
1001
+ `)
1002
+ .all();
1003
+ (0, globals_1.expect)(categoryPatterns.length).toBeGreaterThan(0);
1004
+ // Check feature-auth channel patterns
1005
+ const featurePatterns = categoryPatterns.filter((p) => p.channel === 'feature-auth');
1006
+ const taskPattern = featurePatterns.find((p) => p.category === 'task');
1007
+ (0, globals_1.expect)(taskPattern).toBeDefined();
1008
+ (0, globals_1.expect)(taskPattern.usage_count).toBe(2);
1009
+ (0, globals_1.expect)(taskPattern.usage_pattern).toBe('frequent');
1010
+ });
1011
+ (0, globals_1.it)('should generate actionable insights based on patterns', () => {
1012
+ // Simulate insight generation logic
1013
+ const insights = [];
1014
+ // Check for stale channels
1015
+ const staleChannels = db
1016
+ .prepare(`
1017
+ SELECT
1018
+ channel,
1019
+ julianday('now') - julianday(MAX(updated_at)) as days_inactive
1020
+ FROM context_items
1021
+ GROUP BY channel
1022
+ HAVING days_inactive > 3
1023
+ `)
1024
+ .all();
1025
+ if (staleChannels.length > 0) {
1026
+ insights.push(`${staleChannels.length} channel(s) have been inactive for over 3 days`);
1027
+ }
1028
+ // Check for high-priority concentration
1029
+ const highPriorityStats = db
1030
+ .prepare(`
1031
+ SELECT
1032
+ channel,
1033
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as high_priority_percentage
1034
+ FROM context_items
1035
+ GROUP BY channel
1036
+ HAVING high_priority_percentage > 50
1037
+ `)
1038
+ .all();
1039
+ if (highPriorityStats.length > 0) {
1040
+ insights.push(`${highPriorityStats.length} channel(s) have over 50% high-priority items`);
1041
+ }
1042
+ // Check for single-category channels
1043
+ const singleCategoryChannels = db
1044
+ .prepare(`
1045
+ SELECT
1046
+ channel,
1047
+ COUNT(DISTINCT category) as category_count
1048
+ FROM context_items
1049
+ GROUP BY channel
1050
+ HAVING category_count = 1
1051
+ `)
1052
+ .all();
1053
+ if (singleCategoryChannels.length > 0) {
1054
+ insights.push(`${singleCategoryChannels.length} channel(s) use only a single category`);
1055
+ }
1056
+ (0, globals_1.expect)(insights.length).toBeGreaterThan(0);
1057
+ // At least one insight should be generated from the patterns
1058
+ const hasValidInsight = insights.some(i => i.includes('inactive') || i.includes('high-priority') || i.includes('single category'));
1059
+ (0, globals_1.expect)(hasValidInsight).toBe(true);
1060
+ });
1061
+ });
1062
+ (0, globals_1.describe)('Cross-channel comparisons', () => {
1063
+ (0, globals_1.it)('should compare channels by multiple metrics', () => {
1064
+ const comparison = db
1065
+ .prepare(`
1066
+ SELECT
1067
+ channel,
1068
+ COUNT(*) as item_count,
1069
+ COUNT(DISTINCT session_id) as session_diversity,
1070
+ COUNT(DISTINCT category) as category_diversity,
1071
+ AVG(size) as avg_item_size,
1072
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as high_priority_ratio,
1073
+ julianday('now') - julianday(MIN(created_at)) as channel_age_days,
1074
+ julianday('now') - julianday(MAX(updated_at)) as days_since_update
1075
+ FROM context_items
1076
+ GROUP BY channel
1077
+ ORDER BY item_count DESC
1078
+ `)
1079
+ .all();
1080
+ (0, globals_1.expect)(comparison).toHaveLength(3);
1081
+ // Verify dev-channel has highest diversity
1082
+ const devChannel = comparison.find((c) => c.channel === 'dev-channel');
1083
+ (0, globals_1.expect)(devChannel.session_diversity).toBe(2);
1084
+ (0, globals_1.expect)(devChannel.category_diversity).toBe(5);
1085
+ (0, globals_1.expect)(devChannel.item_count).toBe(5);
1086
+ // Compare with other channels
1087
+ comparison.forEach((channel) => {
1088
+ (0, globals_1.expect)(channel.item_count).toBeGreaterThan(0);
1089
+ (0, globals_1.expect)(channel.high_priority_ratio).toBeGreaterThanOrEqual(0);
1090
+ (0, globals_1.expect)(channel.high_priority_ratio).toBeLessThanOrEqual(100);
1091
+ });
1092
+ });
1093
+ (0, globals_1.it)('should identify channel relationships through shared sessions', () => {
1094
+ const relationships = db
1095
+ .prepare(`
1096
+ WITH channel_sessions AS (
1097
+ SELECT DISTINCT channel, session_id
1098
+ FROM context_items
1099
+ )
1100
+ SELECT
1101
+ cs1.channel as channel1,
1102
+ cs2.channel as channel2,
1103
+ COUNT(DISTINCT cs1.session_id) as shared_sessions
1104
+ FROM channel_sessions cs1
1105
+ JOIN channel_sessions cs2 ON cs1.session_id = cs2.session_id
1106
+ WHERE cs1.channel < cs2.channel
1107
+ GROUP BY cs1.channel, cs2.channel
1108
+ HAVING shared_sessions > 0
1109
+ ORDER BY shared_sessions DESC
1110
+ `)
1111
+ .all();
1112
+ (0, globals_1.expect)(relationships.length).toBeGreaterThan(0);
1113
+ // dev-channel and feature-auth share sessions
1114
+ const devFeatureRel = relationships.find((r) => (r.channel1 === 'dev-channel' && r.channel2 === 'feature-auth') ||
1115
+ (r.channel1 === 'feature-auth' && r.channel2 === 'dev-channel'));
1116
+ (0, globals_1.expect)(devFeatureRel).toBeDefined();
1117
+ (0, globals_1.expect)(devFeatureRel.shared_sessions).toBeGreaterThan(0);
1118
+ });
1119
+ });
1120
+ (0, globals_1.describe)('Error handling and edge cases', () => {
1121
+ (0, globals_1.it)('should handle requests for non-existent channels gracefully', () => {
1122
+ const stats = db
1123
+ .prepare(`
1124
+ SELECT
1125
+ COUNT(*) as item_count,
1126
+ COUNT(DISTINCT session_id) as session_count
1127
+ FROM context_items
1128
+ WHERE channel = ?
1129
+ `)
1130
+ .get('non-existent-channel');
1131
+ (0, globals_1.expect)(stats.item_count).toBe(0);
1132
+ (0, globals_1.expect)(stats.session_count).toBe(0);
1133
+ });
1134
+ (0, globals_1.it)('should handle channels with null or empty categories', () => {
1135
+ // Insert item with null category
1136
+ db.prepare(`
1137
+ INSERT INTO context_items (id, session_id, key, value, channel, category)
1138
+ VALUES (?, ?, ?, ?, ?, NULL)
1139
+ `).run((0, uuid_1.v4)(), testSessionId, 'null-cat-item', 'value', 'test-channel');
1140
+ const categoryStats = db
1141
+ .prepare(`
1142
+ SELECT
1143
+ COALESCE(category, 'uncategorized') as category,
1144
+ COUNT(*) as count
1145
+ FROM context_items
1146
+ WHERE channel = ?
1147
+ GROUP BY category
1148
+ `)
1149
+ .all('test-channel');
1150
+ const uncategorized = categoryStats.find((c) => c.category === 'uncategorized');
1151
+ (0, globals_1.expect)(uncategorized).toBeDefined();
1152
+ (0, globals_1.expect)(uncategorized.count).toBe(1);
1153
+ });
1154
+ (0, globals_1.it)('should handle division by zero in percentage calculations', () => {
1155
+ // Create channel with single item to avoid division issues
1156
+ db.prepare(`
1157
+ INSERT INTO context_items (id, session_id, key, value, channel, priority)
1158
+ VALUES (?, ?, ?, ?, ?, ?)
1159
+ `).run((0, uuid_1.v4)(), testSessionId, 'single-item', 'value', 'single-item-channel', 'high');
1160
+ const stats = db
1161
+ .prepare(`
1162
+ SELECT
1163
+ channel,
1164
+ COUNT(*) as total,
1165
+ SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high_count,
1166
+ CASE
1167
+ WHEN COUNT(*) = 0 THEN 0
1168
+ ELSE ROUND(SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2)
1169
+ END as high_percentage
1170
+ FROM context_items
1171
+ WHERE channel = ?
1172
+ GROUP BY channel
1173
+ `)
1174
+ .get('single-item-channel');
1175
+ (0, globals_1.expect)(stats.total).toBe(1);
1176
+ (0, globals_1.expect)(stats.high_count).toBe(1);
1177
+ (0, globals_1.expect)(stats.high_percentage).toBe(100);
1178
+ });
1179
+ (0, globals_1.it)('should handle very large result sets efficiently', () => {
1180
+ // This is more of a performance consideration test
1181
+ // In real implementation, would need pagination
1182
+ const largeResultTest = db
1183
+ .prepare(`
1184
+ SELECT
1185
+ channel,
1186
+ COUNT(*) as count
1187
+ FROM context_items
1188
+ GROUP BY channel
1189
+ LIMIT 1000
1190
+ `)
1191
+ .all();
1192
+ (0, globals_1.expect)(largeResultTest.length).toBeLessThanOrEqual(1000);
1193
+ });
1194
+ });
1195
+ });
1196
+ (0, globals_1.describe)('Integration between list_channels and channel_stats', () => {
1197
+ (0, globals_1.beforeEach)(() => {
1198
+ // Add some test data for integration tests
1199
+ const items = [
1200
+ {
1201
+ session: testSessionId,
1202
+ key: 'int_test_1',
1203
+ value: 'Integration test 1',
1204
+ channel: 'dev-channel',
1205
+ priority: 'high',
1206
+ category: 'test',
1207
+ },
1208
+ {
1209
+ session: testSessionId,
1210
+ key: 'int_test_2',
1211
+ value: 'Integration test 2',
1212
+ channel: 'dev-channel',
1213
+ priority: 'normal',
1214
+ category: 'test',
1215
+ },
1216
+ {
1217
+ session: testSessionId2,
1218
+ key: 'int_test_3',
1219
+ value: 'Integration test 3',
1220
+ channel: 'prod-channel',
1221
+ priority: 'high',
1222
+ category: 'test',
1223
+ },
1224
+ ];
1225
+ const stmt = db.prepare(`
1226
+ INSERT INTO context_items (id, session_id, key, value, channel, priority, category)
1227
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1228
+ `);
1229
+ for (const item of items) {
1230
+ stmt.run((0, uuid_1.v4)(), item.session, item.key, item.value, item.channel, item.priority, item.category);
1231
+ }
1232
+ });
1233
+ (0, globals_1.it)('should be able to get stats for channels returned by list', () => {
1234
+ // First, list channels
1235
+ const channels = db
1236
+ .prepare(`
1237
+ SELECT DISTINCT channel
1238
+ FROM context_items
1239
+ ORDER BY channel
1240
+ `)
1241
+ .all();
1242
+ (0, globals_1.expect)(channels.length).toBeGreaterThan(0);
1243
+ // Then, get stats for each channel
1244
+ const statsPromises = channels.map((ch) => {
1245
+ const stats = db
1246
+ .prepare(`
1247
+ SELECT
1248
+ COUNT(*) as item_count,
1249
+ COUNT(DISTINCT category) as categories,
1250
+ MAX(updated_at) as last_activity
1251
+ FROM context_items
1252
+ WHERE channel = ?
1253
+ `)
1254
+ .get(ch.channel);
1255
+ return {
1256
+ channel: ch.channel,
1257
+ ...stats,
1258
+ };
1259
+ });
1260
+ const allStats = statsPromises;
1261
+ (0, globals_1.expect)(allStats).toHaveLength(channels.length);
1262
+ allStats.forEach(stat => {
1263
+ (0, globals_1.expect)(stat.item_count).toBeGreaterThan(0);
1264
+ (0, globals_1.expect)(stat.categories).toBeGreaterThan(0);
1265
+ (0, globals_1.expect)(stat.last_activity).toBeDefined();
1266
+ });
1267
+ });
1268
+ (0, globals_1.it)('should provide consistent data between list and stats views', () => {
1269
+ // Get count from list_channels perspective
1270
+ const listData = db
1271
+ .prepare(`
1272
+ SELECT
1273
+ channel,
1274
+ COUNT(*) as list_count
1275
+ FROM context_items
1276
+ WHERE channel = ?
1277
+ GROUP BY channel
1278
+ `)
1279
+ .get('dev-channel');
1280
+ // Get count from channel_stats perspective
1281
+ const statsData = db
1282
+ .prepare(`
1283
+ SELECT COUNT(*) as stats_count
1284
+ FROM context_items
1285
+ WHERE channel = ?
1286
+ `)
1287
+ .get('dev-channel');
1288
+ (0, globals_1.expect)(listData.list_count).toBe(statsData.stats_count);
1289
+ });
1290
+ });
1291
+ });