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,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
|
+
});
|