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,1082 @@
|
|
|
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 database_1 = require("../../utils/database");
|
|
37
|
+
const RepositoryManager_1 = require("../../repositories/RepositoryManager");
|
|
38
|
+
const database_test_helper_1 = require("../helpers/database-test-helper");
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const uuid_1 = require("uuid");
|
|
43
|
+
const timezone_safe_dates_1 = require("../utils/timezone-safe-dates");
|
|
44
|
+
describe('Enhanced Context Operations Integration Tests', () => {
|
|
45
|
+
let dbManager;
|
|
46
|
+
let repositories;
|
|
47
|
+
let tempDbPath;
|
|
48
|
+
let db;
|
|
49
|
+
let testHelper;
|
|
50
|
+
let testSessionId;
|
|
51
|
+
let testSessionId2;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
tempDbPath = path.join(os.tmpdir(), `test-enhanced-context-${Date.now()}.db`);
|
|
54
|
+
dbManager = new database_1.DatabaseManager({
|
|
55
|
+
filename: tempDbPath,
|
|
56
|
+
maxSize: 10 * 1024 * 1024,
|
|
57
|
+
walMode: true,
|
|
58
|
+
});
|
|
59
|
+
db = dbManager.getDatabase();
|
|
60
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
61
|
+
testHelper = new database_test_helper_1.DatabaseTestHelper(db);
|
|
62
|
+
// Create test sessions
|
|
63
|
+
testSessionId = (0, uuid_1.v4)();
|
|
64
|
+
testSessionId2 = (0, uuid_1.v4)();
|
|
65
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session 1');
|
|
66
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId2, 'Test Session 2');
|
|
67
|
+
});
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
dbManager.close();
|
|
70
|
+
try {
|
|
71
|
+
fs.unlinkSync(tempDbPath);
|
|
72
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
73
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
74
|
+
}
|
|
75
|
+
catch (_e) {
|
|
76
|
+
// Ignore
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
describe('Enhanced context_get', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
// Disable triggers to control timestamps precisely
|
|
82
|
+
testHelper.disableTimestampTriggers();
|
|
83
|
+
// Add test data with varying timestamps
|
|
84
|
+
const baseTime = new Date('2024-01-01T00:00:00Z');
|
|
85
|
+
const items = [
|
|
86
|
+
{ key: 'alpha_item', value: 'First value', category: 'task', priority: 'high', offset: 0 },
|
|
87
|
+
{
|
|
88
|
+
key: 'beta_item',
|
|
89
|
+
value: 'Second value',
|
|
90
|
+
category: 'task',
|
|
91
|
+
priority: 'normal',
|
|
92
|
+
offset: 1,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'gamma_item',
|
|
96
|
+
value: 'Third value',
|
|
97
|
+
category: 'decision',
|
|
98
|
+
priority: 'high',
|
|
99
|
+
offset: 2,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
key: 'delta_item',
|
|
103
|
+
value: 'Fourth value with much longer content to test size calculation',
|
|
104
|
+
category: 'note',
|
|
105
|
+
priority: 'low',
|
|
106
|
+
offset: 3,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
key: 'epsilon_item',
|
|
110
|
+
value: 'Fifth value',
|
|
111
|
+
category: 'progress',
|
|
112
|
+
priority: 'normal',
|
|
113
|
+
offset: 4,
|
|
114
|
+
},
|
|
115
|
+
{ key: 'zeta_item', value: 'Sixth value', category: 'task', priority: 'high', offset: 5 },
|
|
116
|
+
{ key: 'eta_item', value: 'Seventh value', category: 'error', priority: 'high', offset: 6 },
|
|
117
|
+
{
|
|
118
|
+
key: 'theta_item',
|
|
119
|
+
value: 'Eighth value',
|
|
120
|
+
category: 'warning',
|
|
121
|
+
priority: 'normal',
|
|
122
|
+
offset: 7,
|
|
123
|
+
},
|
|
124
|
+
{ key: 'iota_item', value: 'Ninth value', category: 'task', priority: 'low', offset: 8 },
|
|
125
|
+
{ key: 'kappa_item', value: 'Tenth value', category: 'note', priority: 'high', offset: 9 },
|
|
126
|
+
];
|
|
127
|
+
items.forEach(item => {
|
|
128
|
+
const createdAt = new Date(baseTime.getTime() + item.offset * 3600000); // 1 hour intervals
|
|
129
|
+
const updatedAt = new Date(createdAt.getTime() + 1800000); // 30 minutes later
|
|
130
|
+
db.prepare(`
|
|
131
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority, created_at, updated_at)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
133
|
+
`).run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, createdAt.toISOString(), updatedAt.toISOString());
|
|
134
|
+
});
|
|
135
|
+
// Add some items to second session for multi-session tests
|
|
136
|
+
db.prepare(`
|
|
137
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority)
|
|
138
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
139
|
+
`).run((0, uuid_1.v4)(), testSessionId2, 'session2_item', 'Another session value', 'task', 'normal');
|
|
140
|
+
// Re-enable triggers
|
|
141
|
+
testHelper.enableTimestampTriggers();
|
|
142
|
+
});
|
|
143
|
+
describe('includeMetadata parameter', () => {
|
|
144
|
+
it('should return items without metadata by default', () => {
|
|
145
|
+
const items = db
|
|
146
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
147
|
+
.all(testSessionId);
|
|
148
|
+
// Simulate response without metadata
|
|
149
|
+
const response = items.map(item => ({
|
|
150
|
+
key: item.key,
|
|
151
|
+
value: item.value,
|
|
152
|
+
category: item.category,
|
|
153
|
+
priority: item.priority,
|
|
154
|
+
}));
|
|
155
|
+
expect(response).toHaveLength(10);
|
|
156
|
+
expect(response[0]).not.toHaveProperty('created_at');
|
|
157
|
+
expect(response[0]).not.toHaveProperty('updated_at');
|
|
158
|
+
expect(response[0]).not.toHaveProperty('size');
|
|
159
|
+
expect(response[0]).not.toHaveProperty('session_info');
|
|
160
|
+
});
|
|
161
|
+
it('should include metadata when includeMetadata is true', () => {
|
|
162
|
+
const items = db
|
|
163
|
+
.prepare(`
|
|
164
|
+
SELECT ci.*, s.name as session_name, s.description as session_description
|
|
165
|
+
FROM context_items ci
|
|
166
|
+
JOIN sessions s ON ci.session_id = s.id
|
|
167
|
+
WHERE ci.session_id = ?
|
|
168
|
+
`)
|
|
169
|
+
.all(testSessionId);
|
|
170
|
+
// Simulate response with metadata
|
|
171
|
+
const response = items.map(item => ({
|
|
172
|
+
key: item.key,
|
|
173
|
+
value: item.value,
|
|
174
|
+
category: item.category,
|
|
175
|
+
priority: item.priority,
|
|
176
|
+
metadata: {
|
|
177
|
+
created_at: item.created_at,
|
|
178
|
+
updated_at: item.updated_at,
|
|
179
|
+
size: Buffer.byteLength(item.value, 'utf8'),
|
|
180
|
+
session_info: {
|
|
181
|
+
id: item.session_id,
|
|
182
|
+
name: item.session_name,
|
|
183
|
+
description: item.session_description,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}));
|
|
187
|
+
expect(response).toHaveLength(10);
|
|
188
|
+
expect(response[0].metadata).toBeDefined();
|
|
189
|
+
expect(response[0].metadata.created_at).toBeDefined();
|
|
190
|
+
expect(response[0].metadata.updated_at).toBeDefined();
|
|
191
|
+
expect(response[0].metadata.size).toBeGreaterThan(0);
|
|
192
|
+
expect(response[0].metadata.session_info.name).toBe('Test Session 1');
|
|
193
|
+
});
|
|
194
|
+
it('should calculate correct size for items with metadata', () => {
|
|
195
|
+
const item = db
|
|
196
|
+
.prepare('SELECT * FROM context_items WHERE key = ?')
|
|
197
|
+
.get('delta_item');
|
|
198
|
+
const size = Buffer.byteLength(item.value, 'utf8');
|
|
199
|
+
expect(size).toBe(62); // Byte length of "Fourth value with much longer content to test size calculation"
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe('sort parameter', () => {
|
|
203
|
+
it('should sort by created_at descending (newest first)', () => {
|
|
204
|
+
const items = db
|
|
205
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
|
|
206
|
+
.all(testSessionId);
|
|
207
|
+
expect(items[0].key).toBe('kappa_item'); // Last created
|
|
208
|
+
expect(items[9].key).toBe('alpha_item'); // First created
|
|
209
|
+
});
|
|
210
|
+
it('should sort by created_at ascending (oldest first)', () => {
|
|
211
|
+
const items = db
|
|
212
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at ASC')
|
|
213
|
+
.all(testSessionId);
|
|
214
|
+
expect(items[0].key).toBe('alpha_item'); // First created
|
|
215
|
+
expect(items[9].key).toBe('kappa_item'); // Last created
|
|
216
|
+
});
|
|
217
|
+
it('should sort by updated_at descending', () => {
|
|
218
|
+
// Disable triggers to control timestamps precisely
|
|
219
|
+
testHelper.disableTimestampTriggers();
|
|
220
|
+
// Update a specific item to have a more recent updated_at
|
|
221
|
+
const recentUpdate = new Date();
|
|
222
|
+
db.prepare('UPDATE context_items SET updated_at = ? WHERE key = ?').run(recentUpdate.toISOString(), 'beta_item');
|
|
223
|
+
const items = db
|
|
224
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY updated_at DESC')
|
|
225
|
+
.all(testSessionId);
|
|
226
|
+
// Re-enable triggers
|
|
227
|
+
testHelper.enableTimestampTriggers();
|
|
228
|
+
expect(items[0].key).toBe('beta_item'); // Most recently updated
|
|
229
|
+
});
|
|
230
|
+
it('should sort by key ascending', () => {
|
|
231
|
+
const items = db
|
|
232
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC')
|
|
233
|
+
.all(testSessionId);
|
|
234
|
+
expect(items[0].key).toBe('alpha_item');
|
|
235
|
+
expect(items[1].key).toBe('beta_item');
|
|
236
|
+
expect(items[9].key).toBe('zeta_item');
|
|
237
|
+
});
|
|
238
|
+
it('should sort by key descending', () => {
|
|
239
|
+
const items = db
|
|
240
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key DESC')
|
|
241
|
+
.all(testSessionId);
|
|
242
|
+
expect(items[0].key).toBe('zeta_item');
|
|
243
|
+
expect(items[9].key).toBe('alpha_item');
|
|
244
|
+
});
|
|
245
|
+
it('should default to created_at DESC when sort is not specified', () => {
|
|
246
|
+
const items = db
|
|
247
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
|
|
248
|
+
.all(testSessionId);
|
|
249
|
+
expect(items[0].key).toBe('kappa_item');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
describe('limit and offset parameters', () => {
|
|
253
|
+
it('should return limited number of items', () => {
|
|
254
|
+
const result = repositories.contexts.queryEnhanced({
|
|
255
|
+
sessionId: testSessionId,
|
|
256
|
+
sort: 'key_asc',
|
|
257
|
+
limit: 5,
|
|
258
|
+
});
|
|
259
|
+
expect(result.items).toHaveLength(5);
|
|
260
|
+
expect(result.items[0].key).toBe('alpha_item');
|
|
261
|
+
expect(result.items[4].key).toBe('eta_item'); // 5th item alphabetically
|
|
262
|
+
});
|
|
263
|
+
it('should apply offset for pagination', () => {
|
|
264
|
+
const result = repositories.contexts.queryEnhanced({
|
|
265
|
+
sessionId: testSessionId,
|
|
266
|
+
sort: 'key_asc',
|
|
267
|
+
limit: 5,
|
|
268
|
+
offset: 5,
|
|
269
|
+
});
|
|
270
|
+
expect(result.items).toHaveLength(5);
|
|
271
|
+
expect(result.items[0].key).toBe('gamma_item'); // 6th item alphabetically (offset 5)
|
|
272
|
+
expect(result.items[4].key).toBe('theta_item'); // Last item in the result set
|
|
273
|
+
});
|
|
274
|
+
it('should handle limit larger than available items', () => {
|
|
275
|
+
const items = db
|
|
276
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 20')
|
|
277
|
+
.all(testSessionId);
|
|
278
|
+
expect(items).toHaveLength(10); // Only 10 items available
|
|
279
|
+
});
|
|
280
|
+
it('should handle offset beyond available items', () => {
|
|
281
|
+
const items = db
|
|
282
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 5 OFFSET 15')
|
|
283
|
+
.all(testSessionId);
|
|
284
|
+
expect(items).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
it('should return total count for pagination info', () => {
|
|
287
|
+
const totalCount = db
|
|
288
|
+
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
|
|
289
|
+
.get(testSessionId);
|
|
290
|
+
const items = db
|
|
291
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key ASC LIMIT 5')
|
|
292
|
+
.all(testSessionId);
|
|
293
|
+
expect(totalCount.count).toBe(10);
|
|
294
|
+
expect(items).toHaveLength(5);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe('createdAfter and createdBefore parameters', () => {
|
|
298
|
+
it('should filter items created after a specific date', () => {
|
|
299
|
+
const afterDate = new Date('2024-01-01T03:00:00Z'); // After first 3 items
|
|
300
|
+
const result = repositories.contexts.queryEnhanced({
|
|
301
|
+
sessionId: testSessionId,
|
|
302
|
+
createdAfter: afterDate.toISOString(),
|
|
303
|
+
sort: 'created_at_asc',
|
|
304
|
+
});
|
|
305
|
+
expect(result.items).toHaveLength(7); // Items created after 03:00 (includes public items from other sessions)
|
|
306
|
+
expect(result.items[0].key).toBe('epsilon_item'); // First item after 03:00 is at 04:00
|
|
307
|
+
});
|
|
308
|
+
it('should filter items created before a specific date', () => {
|
|
309
|
+
const beforeDate = new Date('2024-01-01T05:00:00Z'); // Before last 5 items
|
|
310
|
+
const items = db
|
|
311
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at < ? ORDER BY created_at ASC')
|
|
312
|
+
.all(testSessionId, beforeDate.toISOString());
|
|
313
|
+
expect(items).toHaveLength(5);
|
|
314
|
+
expect(items[4].key).toBe('epsilon_item');
|
|
315
|
+
});
|
|
316
|
+
it('should filter items within a date range', () => {
|
|
317
|
+
const afterDate = new Date('2024-01-01T02:00:00Z');
|
|
318
|
+
const beforeDate = new Date('2024-01-01T06:00:00Z');
|
|
319
|
+
const result = repositories.contexts.queryEnhanced({
|
|
320
|
+
sessionId: testSessionId,
|
|
321
|
+
createdAfter: afterDate.toISOString(),
|
|
322
|
+
createdBefore: beforeDate.toISOString(),
|
|
323
|
+
sort: 'created_at_asc',
|
|
324
|
+
});
|
|
325
|
+
expect(result.items).toHaveLength(3); // Items with 02:00 < created_at < 06:00
|
|
326
|
+
expect(result.items[0].key).toBe('delta_item'); // 03:00
|
|
327
|
+
expect(result.items[2].key).toBe('zeta_item'); // 05:00
|
|
328
|
+
});
|
|
329
|
+
it('should return empty when no items match date filters', () => {
|
|
330
|
+
const afterDate = new Date('2025-01-01T00:00:00Z'); // Future date
|
|
331
|
+
const items = db
|
|
332
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
|
|
333
|
+
.all(testSessionId, afterDate.toISOString());
|
|
334
|
+
expect(items).toHaveLength(0);
|
|
335
|
+
});
|
|
336
|
+
it('should handle invalid date formats gracefully', () => {
|
|
337
|
+
// This test expects the implementation to validate date formats
|
|
338
|
+
const invalidDate = 'not-a-date';
|
|
339
|
+
// The implementation should either throw an error or return all items
|
|
340
|
+
// For now, we'll test that it doesn't crash
|
|
341
|
+
expect(() => {
|
|
342
|
+
db.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?').all(testSessionId, invalidDate);
|
|
343
|
+
}).not.toThrow();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
describe('keyPattern parameter', () => {
|
|
347
|
+
it('should match items by simple pattern', () => {
|
|
348
|
+
const items = db
|
|
349
|
+
.prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE '%_item' ORDER BY key ASC")
|
|
350
|
+
.all(testSessionId);
|
|
351
|
+
expect(items).toHaveLength(10); // All items end with '_item'
|
|
352
|
+
});
|
|
353
|
+
it('should match items by prefix pattern', () => {
|
|
354
|
+
// Simulate regex ^alpha.* as SQL LIKE
|
|
355
|
+
const items = db
|
|
356
|
+
.prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE 'alpha%' ORDER BY key ASC")
|
|
357
|
+
.all(testSessionId);
|
|
358
|
+
expect(items).toHaveLength(1);
|
|
359
|
+
expect(items[0].key).toBe('alpha_item');
|
|
360
|
+
});
|
|
361
|
+
it('should match items by complex pattern', () => {
|
|
362
|
+
// Simulate regex that matches Greek letter names
|
|
363
|
+
const items = db
|
|
364
|
+
.prepare("SELECT * FROM context_items WHERE session_id = ? AND (key LIKE 'alpha%' OR key LIKE 'beta%' OR key LIKE 'gamma%') ORDER BY key ASC")
|
|
365
|
+
.all(testSessionId);
|
|
366
|
+
expect(items).toHaveLength(3);
|
|
367
|
+
expect(items.map(i => i.key)).toEqual(['alpha_item', 'beta_item', 'gamma_item']);
|
|
368
|
+
});
|
|
369
|
+
it('should return empty when pattern matches nothing', () => {
|
|
370
|
+
const items = db
|
|
371
|
+
.prepare("SELECT * FROM context_items WHERE session_id = ? AND key LIKE 'nonexistent%'")
|
|
372
|
+
.all(testSessionId);
|
|
373
|
+
expect(items).toHaveLength(0);
|
|
374
|
+
});
|
|
375
|
+
it('should handle special regex characters', () => {
|
|
376
|
+
// Add item with special characters in key
|
|
377
|
+
db.prepare(`
|
|
378
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority)
|
|
379
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
380
|
+
`).run((0, uuid_1.v4)(), testSessionId, 'item.with.dots', 'Special value', 'note', 'normal');
|
|
381
|
+
// Pattern should escape dots when used literally
|
|
382
|
+
const items = db
|
|
383
|
+
.prepare("SELECT * FROM context_items WHERE session_id = ? AND key = 'item.with.dots'")
|
|
384
|
+
.all(testSessionId);
|
|
385
|
+
expect(items).toHaveLength(1);
|
|
386
|
+
expect(items[0].key).toBe('item.with.dots');
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
describe('priorities parameter', () => {
|
|
390
|
+
it('should filter by single priority', () => {
|
|
391
|
+
const items = db
|
|
392
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ? ORDER BY key ASC')
|
|
393
|
+
.all(testSessionId, 'high');
|
|
394
|
+
expect(items).toHaveLength(5);
|
|
395
|
+
expect(items.every(i => i.priority === 'high')).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
it('should filter by multiple priorities', () => {
|
|
398
|
+
const items = db
|
|
399
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND priority IN (?, ?) ORDER BY key ASC')
|
|
400
|
+
.all(testSessionId, 'high', 'normal');
|
|
401
|
+
expect(items).toHaveLength(8); // 5 high + 3 normal
|
|
402
|
+
expect(items.every(i => ['high', 'normal'].includes(i.priority))).toBe(true);
|
|
403
|
+
});
|
|
404
|
+
it('should return empty when priorities array is empty', () => {
|
|
405
|
+
// Simulate empty priorities filter - should return all items
|
|
406
|
+
const items = db
|
|
407
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
408
|
+
.all(testSessionId);
|
|
409
|
+
expect(items).toHaveLength(10);
|
|
410
|
+
});
|
|
411
|
+
it('should handle invalid priority values', () => {
|
|
412
|
+
const items = db
|
|
413
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ?')
|
|
414
|
+
.all(testSessionId, 'invalid_priority');
|
|
415
|
+
expect(items).toHaveLength(0);
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
describe('Combining multiple parameters', () => {
|
|
419
|
+
it('should combine category filter with sort and limit', () => {
|
|
420
|
+
const items = db
|
|
421
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ? ORDER BY key DESC LIMIT 2')
|
|
422
|
+
.all(testSessionId, 'task');
|
|
423
|
+
expect(items).toHaveLength(2);
|
|
424
|
+
expect(items[0].key).toBe('zeta_item');
|
|
425
|
+
expect(items[1].key).toBe('iota_item');
|
|
426
|
+
expect(items.every(i => i.category === 'task')).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
it('should combine date filters with priority and pagination', () => {
|
|
429
|
+
const afterDate = new Date('2024-01-01T01:00:00Z');
|
|
430
|
+
const beforeDate = new Date('2024-01-01T07:00:00Z');
|
|
431
|
+
const items = db
|
|
432
|
+
.prepare(`
|
|
433
|
+
SELECT * FROM context_items
|
|
434
|
+
WHERE session_id = ?
|
|
435
|
+
AND created_at > ?
|
|
436
|
+
AND created_at < ?
|
|
437
|
+
AND priority = ?
|
|
438
|
+
ORDER BY created_at ASC
|
|
439
|
+
LIMIT 3 OFFSET 1
|
|
440
|
+
`)
|
|
441
|
+
.all(testSessionId, afterDate.toISOString(), beforeDate.toISOString(), 'high');
|
|
442
|
+
expect(items).toHaveLength(2); // Only 3 high priority items in range, offset 1
|
|
443
|
+
});
|
|
444
|
+
it('should combine keyPattern with category and sort', () => {
|
|
445
|
+
// Pattern matching items starting with vowels (a, e, i)
|
|
446
|
+
const result = repositories.contexts.queryEnhanced({
|
|
447
|
+
sessionId: testSessionId,
|
|
448
|
+
keyPattern: '[aei]*', // SQLite GLOB pattern for starts with a, e, or i
|
|
449
|
+
category: 'task',
|
|
450
|
+
sort: 'key_asc',
|
|
451
|
+
});
|
|
452
|
+
expect(result.items).toHaveLength(2); // alpha (task), iota (task)
|
|
453
|
+
expect(result.items[0].key).toBe('alpha_item');
|
|
454
|
+
expect(result.items[1].key).toBe('iota_item');
|
|
455
|
+
});
|
|
456
|
+
it('should include metadata with all filters applied', () => {
|
|
457
|
+
const items = db
|
|
458
|
+
.prepare(`
|
|
459
|
+
SELECT ci.*, s.name as session_name, LENGTH(ci.value) as value_size
|
|
460
|
+
FROM context_items ci
|
|
461
|
+
JOIN sessions s ON ci.session_id = s.id
|
|
462
|
+
WHERE ci.session_id = ?
|
|
463
|
+
AND ci.priority IN (?, ?)
|
|
464
|
+
AND ci.category = ?
|
|
465
|
+
ORDER BY ci.created_at DESC
|
|
466
|
+
LIMIT 2
|
|
467
|
+
`)
|
|
468
|
+
.all(testSessionId, 'high', 'normal', 'task');
|
|
469
|
+
expect(items).toHaveLength(2);
|
|
470
|
+
expect(items[0]).toHaveProperty('value_size');
|
|
471
|
+
expect(items[0]).toHaveProperty('session_name');
|
|
472
|
+
expect(items[0].session_name).toBe('Test Session 1');
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
describe('Backward compatibility', () => {
|
|
476
|
+
it('should work with only key parameter as before', () => {
|
|
477
|
+
const item = db
|
|
478
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
|
|
479
|
+
.get(testSessionId, 'alpha_item');
|
|
480
|
+
expect(item).toBeDefined();
|
|
481
|
+
expect(item.value).toBe('First value');
|
|
482
|
+
});
|
|
483
|
+
it('should work with only category parameter as before', () => {
|
|
484
|
+
const items = db
|
|
485
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
|
|
486
|
+
.all(testSessionId, 'task');
|
|
487
|
+
expect(items).toHaveLength(4);
|
|
488
|
+
expect(items.every(i => i.category === 'task')).toBe(true);
|
|
489
|
+
});
|
|
490
|
+
it('should work with session_id parameter as before', () => {
|
|
491
|
+
const items = db
|
|
492
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
493
|
+
.all(testSessionId2);
|
|
494
|
+
expect(items).toHaveLength(1);
|
|
495
|
+
expect(items[0].key).toBe('session2_item');
|
|
496
|
+
});
|
|
497
|
+
it('should return empty array when no matches as before', () => {
|
|
498
|
+
const items = db
|
|
499
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
|
|
500
|
+
.all(testSessionId, 'nonexistent');
|
|
501
|
+
expect(items).toHaveLength(0);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
describe('Edge cases', () => {
|
|
505
|
+
it('should handle empty session', () => {
|
|
506
|
+
const emptySessionId = (0, uuid_1.v4)();
|
|
507
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
|
|
508
|
+
const items = db
|
|
509
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
510
|
+
.all(emptySessionId);
|
|
511
|
+
expect(items).toHaveLength(0);
|
|
512
|
+
});
|
|
513
|
+
it('should handle very large limit values', () => {
|
|
514
|
+
const items = db
|
|
515
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? LIMIT 999999')
|
|
516
|
+
.all(testSessionId);
|
|
517
|
+
expect(items).toHaveLength(10); // Still returns only available items
|
|
518
|
+
});
|
|
519
|
+
it('should handle negative offset gracefully', () => {
|
|
520
|
+
// SQLite treats negative offset as 0
|
|
521
|
+
const items = db
|
|
522
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? LIMIT 5 OFFSET -5')
|
|
523
|
+
.all(testSessionId);
|
|
524
|
+
expect(items).toHaveLength(5);
|
|
525
|
+
});
|
|
526
|
+
it('should handle concurrent access with proper isolation', () => {
|
|
527
|
+
// Simulate concurrent reads
|
|
528
|
+
const promises = Array(5)
|
|
529
|
+
.fill(null)
|
|
530
|
+
.map(() => new Promise(resolve => {
|
|
531
|
+
const items = db
|
|
532
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
533
|
+
.all(testSessionId);
|
|
534
|
+
resolve(items.length);
|
|
535
|
+
}));
|
|
536
|
+
return Promise.all(promises).then(results => {
|
|
537
|
+
expect(results).toEqual([10, 10, 10, 10, 10]);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
describe('Enhanced context_timeline', () => {
|
|
543
|
+
beforeEach(() => {
|
|
544
|
+
// TIMEZONE-SAFE: Use fixed UTC dates to ensure consistent behavior across all environments
|
|
545
|
+
(0, timezone_safe_dates_1.validateTimezoneSafety)();
|
|
546
|
+
const { today: _today, yesterday: _yesterday } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
547
|
+
// Create timezone-safe timestamps at specific hours
|
|
548
|
+
const yesterdayMorning = (0, timezone_safe_dates_1.createUTCDate)(-1, 10, 0, 0); // 10 AM yesterday UTC
|
|
549
|
+
const yesterdayAfternoon = (0, timezone_safe_dates_1.createUTCDate)(-1, 15, 0, 0); // 3 PM yesterday UTC
|
|
550
|
+
const todayEarlyMorning = (0, timezone_safe_dates_1.createUTCDate)(0, 5, 0, 0); // 5 AM today UTC
|
|
551
|
+
const todayEarlyMorning2 = (0, timezone_safe_dates_1.createUTCDate)(0, 7, 0, 0); // 7 AM today UTC
|
|
552
|
+
const todayMorning = (0, timezone_safe_dates_1.createUTCDate)(0, 9, 0, 0); // 9 AM today UTC
|
|
553
|
+
const todayAfternoon = (0, timezone_safe_dates_1.createUTCDate)(0, 14, 0, 0); // 2 PM today UTC
|
|
554
|
+
const timeOffsets = [
|
|
555
|
+
// Use timezone-safe absolute timestamps
|
|
556
|
+
{ timestamp: todayMorning, key: 'recent_1', category: 'task' }, // 9 AM today
|
|
557
|
+
{ timestamp: todayAfternoon, key: 'recent_2', category: 'note' }, // 2 PM today
|
|
558
|
+
{ timestamp: todayEarlyMorning2, key: 'today_1', category: 'task' }, // 7 AM today
|
|
559
|
+
{ timestamp: todayEarlyMorning, key: 'today_2', category: 'decision' }, // 5 AM today
|
|
560
|
+
{ timestamp: yesterdayMorning, key: 'yesterday_1', category: 'task' }, // Yesterday 10 AM
|
|
561
|
+
{ timestamp: yesterdayAfternoon, key: 'yesterday_2', category: 'note' }, // Yesterday 3 PM
|
|
562
|
+
{ timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-72), key: 'days_ago_1', category: 'progress' }, // 3 days ago
|
|
563
|
+
{ timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-168), key: 'week_ago_1', category: 'task' }, // 1 week ago
|
|
564
|
+
{ timestamp: (0, timezone_safe_dates_1.createUTCDateByHours)(-336), key: 'weeks_ago_1', category: 'error' }, // 2 weeks ago
|
|
565
|
+
];
|
|
566
|
+
timeOffsets.forEach(({ timestamp, key, category }) => {
|
|
567
|
+
const createdAt = timestamp;
|
|
568
|
+
db.prepare(`
|
|
569
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
|
|
570
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
571
|
+
`).run((0, uuid_1.v4)(), testSessionId, key, `Value for ${key}`, category, 'normal', createdAt.toISOString());
|
|
572
|
+
});
|
|
573
|
+
// Add journal entries
|
|
574
|
+
const journalEntries = [
|
|
575
|
+
{ hours: -3, entry: 'Completed major refactoring', mood: 'accomplished' },
|
|
576
|
+
{ hours: -24, entry: 'Started new feature branch', mood: 'excited' },
|
|
577
|
+
{ hours: -48, entry: 'Fixed critical bug', mood: 'relieved' },
|
|
578
|
+
];
|
|
579
|
+
journalEntries.forEach(({ hours, entry, mood }) => {
|
|
580
|
+
const createdAt = (0, timezone_safe_dates_1.createUTCDateByHours)(hours);
|
|
581
|
+
db.prepare(`
|
|
582
|
+
INSERT INTO journal_entries (id, session_id, entry, mood, created_at)
|
|
583
|
+
VALUES (?, ?, ?, ?, ?)
|
|
584
|
+
`).run((0, uuid_1.v4)(), testSessionId, entry, mood, createdAt.toISOString());
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
describe('Basic timeline functionality', () => {
|
|
588
|
+
it('should group items by time period', () => {
|
|
589
|
+
// TIMEZONE-SAFE: Use UTC-based date calculations
|
|
590
|
+
const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
591
|
+
const oneDayAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-24);
|
|
592
|
+
const oneWeekAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-7 * 24);
|
|
593
|
+
// Get items from different periods
|
|
594
|
+
const todayItems = db
|
|
595
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
|
|
596
|
+
.all(testSessionId, oneDayAgo.toISOString());
|
|
597
|
+
const thisWeekItems = db
|
|
598
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ? AND created_at <= ?')
|
|
599
|
+
.all(testSessionId, oneWeekAgo.toISOString(), oneDayAgo.toISOString());
|
|
600
|
+
expect(todayItems.length).toBeGreaterThan(0);
|
|
601
|
+
expect(thisWeekItems.length).toBeGreaterThan(0);
|
|
602
|
+
});
|
|
603
|
+
it('should order timeline entries by date descending', () => {
|
|
604
|
+
const items = db
|
|
605
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
|
|
606
|
+
.all(testSessionId);
|
|
607
|
+
// Verify ordering
|
|
608
|
+
for (let i = 1; i < items.length; i++) {
|
|
609
|
+
const prevDate = new Date(items[i - 1].created_at);
|
|
610
|
+
const currDate = new Date(items[i].created_at);
|
|
611
|
+
expect(prevDate.getTime()).toBeGreaterThanOrEqual(currDate.getTime());
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
describe('includeItems parameter', () => {
|
|
616
|
+
it('should return timeline without item details by default', () => {
|
|
617
|
+
// Simulate timeline response without items
|
|
618
|
+
const periods = db
|
|
619
|
+
.prepare(`
|
|
620
|
+
SELECT
|
|
621
|
+
DATE(created_at) as period,
|
|
622
|
+
COUNT(*) as item_count
|
|
623
|
+
FROM context_items
|
|
624
|
+
WHERE session_id = ?
|
|
625
|
+
GROUP BY DATE(created_at)
|
|
626
|
+
ORDER BY period DESC
|
|
627
|
+
`)
|
|
628
|
+
.all(testSessionId);
|
|
629
|
+
expect(periods.length).toBeGreaterThan(0);
|
|
630
|
+
expect(periods[0]).toHaveProperty('period');
|
|
631
|
+
expect(periods[0]).toHaveProperty('item_count');
|
|
632
|
+
expect(periods[0]).not.toHaveProperty('items');
|
|
633
|
+
});
|
|
634
|
+
it('should include item details when includeItems is true', () => {
|
|
635
|
+
// Get timeline with items
|
|
636
|
+
const periods = db
|
|
637
|
+
.prepare(`
|
|
638
|
+
SELECT DATE(created_at) as period
|
|
639
|
+
FROM context_items
|
|
640
|
+
WHERE session_id = ?
|
|
641
|
+
GROUP BY DATE(created_at)
|
|
642
|
+
ORDER BY period DESC
|
|
643
|
+
`)
|
|
644
|
+
.all(testSessionId);
|
|
645
|
+
const timeline = periods.map(period => {
|
|
646
|
+
const items = db
|
|
647
|
+
.prepare(`
|
|
648
|
+
SELECT * FROM context_items
|
|
649
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
650
|
+
ORDER BY created_at DESC
|
|
651
|
+
`)
|
|
652
|
+
.all(testSessionId, period.period);
|
|
653
|
+
return {
|
|
654
|
+
period: period.period,
|
|
655
|
+
item_count: items.length,
|
|
656
|
+
items: items.map(item => ({
|
|
657
|
+
key: item.key,
|
|
658
|
+
value: item.value,
|
|
659
|
+
category: item.category,
|
|
660
|
+
priority: item.priority,
|
|
661
|
+
created_at: item.created_at,
|
|
662
|
+
})),
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
expect(timeline.length).toBeGreaterThan(0);
|
|
666
|
+
expect(timeline[0].items).toBeDefined();
|
|
667
|
+
expect(timeline[0].items.length).toBe(timeline[0].item_count);
|
|
668
|
+
});
|
|
669
|
+
it('should include journal entries in timeline', () => {
|
|
670
|
+
const journals = db
|
|
671
|
+
.prepare('SELECT * FROM journal_entries WHERE session_id = ? ORDER BY created_at DESC')
|
|
672
|
+
.all(testSessionId);
|
|
673
|
+
expect(journals).toHaveLength(3);
|
|
674
|
+
expect(journals[0]).toHaveProperty('entry');
|
|
675
|
+
expect(journals[0]).toHaveProperty('mood');
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
describe('categories parameter', () => {
|
|
679
|
+
it('should filter timeline by single category', () => {
|
|
680
|
+
const taskItems = db
|
|
681
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
|
|
682
|
+
.all(testSessionId, 'task');
|
|
683
|
+
expect(taskItems.length).toBeGreaterThan(0);
|
|
684
|
+
expect(taskItems.every(item => item.category === 'task')).toBe(true);
|
|
685
|
+
});
|
|
686
|
+
it('should filter timeline by multiple categories', () => {
|
|
687
|
+
const items = db
|
|
688
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND category IN (?, ?)')
|
|
689
|
+
.all(testSessionId, 'task', 'note');
|
|
690
|
+
expect(items.length).toBeGreaterThan(0);
|
|
691
|
+
expect(items.every(item => ['task', 'note'].includes(item.category))).toBe(true);
|
|
692
|
+
});
|
|
693
|
+
it('should return empty timeline when no items match categories', () => {
|
|
694
|
+
const items = db
|
|
695
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
|
|
696
|
+
.all(testSessionId, 'nonexistent_category');
|
|
697
|
+
expect(items).toHaveLength(0);
|
|
698
|
+
});
|
|
699
|
+
it('should include all categories when parameter is empty', () => {
|
|
700
|
+
const allItems = db
|
|
701
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
702
|
+
.all(testSessionId);
|
|
703
|
+
const categories = [...new Set(allItems.map(item => item.category))];
|
|
704
|
+
expect(categories.length).toBeGreaterThan(3);
|
|
705
|
+
});
|
|
706
|
+
});
|
|
707
|
+
describe('relativeTime parameter', () => {
|
|
708
|
+
it('should handle "today" relative time', () => {
|
|
709
|
+
// TIMEZONE-SAFE: Use UTC start of day
|
|
710
|
+
const todayStart = (0, timezone_safe_dates_1.createUTCDate)(0, 0, 0, 0);
|
|
711
|
+
const items = db
|
|
712
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
|
|
713
|
+
.all(testSessionId, todayStart.toISOString());
|
|
714
|
+
expect(items.length).toBeGreaterThan(0);
|
|
715
|
+
// Check for items we know are created "today" based on our test setup
|
|
716
|
+
// These items use specific timestamps that should be within today
|
|
717
|
+
const todayKeys = items.map(i => i.key);
|
|
718
|
+
// At minimum, we should have items created with relative hours that fall within today
|
|
719
|
+
expect(todayKeys.some(key => key.includes('recent') || key.includes('today'))).toBe(true);
|
|
720
|
+
});
|
|
721
|
+
it('should handle "yesterday" relative time', () => {
|
|
722
|
+
// TIMEZONE-SAFE: Use UTC dates for day boundaries
|
|
723
|
+
const yesterdayStart = (0, timezone_safe_dates_1.createUTCDate)(-1, 0, 0, 0);
|
|
724
|
+
const todayStart = (0, timezone_safe_dates_1.createUTCDate)(0, 0, 0, 0);
|
|
725
|
+
const items = db
|
|
726
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at < ?')
|
|
727
|
+
.all(testSessionId, yesterdayStart.toISOString(), todayStart.toISOString());
|
|
728
|
+
expect(items.some(i => i.key.includes('yesterday'))).toBe(true);
|
|
729
|
+
});
|
|
730
|
+
it('should handle "X hours ago" format', () => {
|
|
731
|
+
// TIMEZONE-SAFE: Use UTC-based hour calculations
|
|
732
|
+
const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
733
|
+
const oneHourAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-1);
|
|
734
|
+
const twoHoursAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-2);
|
|
735
|
+
const fiveHoursAgo = (0, timezone_safe_dates_1.createUTCDateByHours)(-5);
|
|
736
|
+
// Add test items with specific relative timestamps
|
|
737
|
+
db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
|
|
738
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_1h_ago', 'One hour ago', 'test', 'normal', oneHourAgo.toISOString());
|
|
739
|
+
db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
|
|
740
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_2h_ago', 'Two hours ago', 'test', 'normal', twoHoursAgo.toISOString());
|
|
741
|
+
db.prepare(`INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
|
|
742
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, 'test_5h_ago', 'Five hours ago', 'test', 'normal', fiveHoursAgo.toISOString());
|
|
743
|
+
// Query for items created 2 hours ago or less
|
|
744
|
+
const queryTime = (0, timezone_safe_dates_1.createUTCDateByHours)(-2.1); // 2.1 hours ago to ensure we catch 2 hour old items
|
|
745
|
+
const items = db
|
|
746
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
|
|
747
|
+
.all(testSessionId, queryTime.toISOString());
|
|
748
|
+
// Check what items we actually got
|
|
749
|
+
const itemKeys = items.map(i => i.key);
|
|
750
|
+
// Should include items created 2 hours ago or less
|
|
751
|
+
expect(itemKeys).toContain('test_1h_ago');
|
|
752
|
+
expect(itemKeys).toContain('test_2h_ago');
|
|
753
|
+
// Should not include items created more than 2 hours ago
|
|
754
|
+
expect(itemKeys).not.toContain('test_5h_ago');
|
|
755
|
+
});
|
|
756
|
+
it('should handle "X days ago" format', () => {
|
|
757
|
+
// TIMEZONE-SAFE: Use UTC date arithmetic
|
|
758
|
+
const threeDaysAgo = (0, timezone_safe_dates_1.createUTCDate)(-3);
|
|
759
|
+
const items = db
|
|
760
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
|
|
761
|
+
.all(testSessionId, threeDaysAgo.toISOString());
|
|
762
|
+
expect(items.length).toBeGreaterThan(0);
|
|
763
|
+
// Should include items created within the last 3 days
|
|
764
|
+
expect(items.some(i => i.key === 'recent_1' ||
|
|
765
|
+
i.key === 'recent_2' ||
|
|
766
|
+
i.key === 'today_1' ||
|
|
767
|
+
i.key === 'today_2')).toBe(true);
|
|
768
|
+
});
|
|
769
|
+
it('should handle "this week" relative time', () => {
|
|
770
|
+
// TIMEZONE-SAFE: Calculate start of week in UTC
|
|
771
|
+
const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
772
|
+
const dayOfWeek = _today.getUTCDay();
|
|
773
|
+
const startOfWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek, 0, 0, 0);
|
|
774
|
+
const items = db
|
|
775
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
|
|
776
|
+
.all(testSessionId, startOfWeek.toISOString());
|
|
777
|
+
expect(items.length).toBeGreaterThan(0);
|
|
778
|
+
});
|
|
779
|
+
it('should handle "last week" relative time', () => {
|
|
780
|
+
// TIMEZONE-SAFE: Calculate last week's boundaries in UTC
|
|
781
|
+
const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
782
|
+
const dayOfWeek = _today.getUTCDay();
|
|
783
|
+
const startOfLastWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek - 7, 0, 0, 0);
|
|
784
|
+
const endOfLastWeek = (0, timezone_safe_dates_1.createUTCDate)(-dayOfWeek, 0, 0, 0);
|
|
785
|
+
const items = db
|
|
786
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at < ?')
|
|
787
|
+
.all(testSessionId, startOfLastWeek.toISOString(), endOfLastWeek.toISOString());
|
|
788
|
+
expect(items.some(i => i.key === 'week_ago_1')).toBe(true);
|
|
789
|
+
});
|
|
790
|
+
it('should default to all time when relativeTime is invalid', () => {
|
|
791
|
+
// Invalid relative time should return all items
|
|
792
|
+
const items = db
|
|
793
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
794
|
+
.all(testSessionId);
|
|
795
|
+
expect(items).toHaveLength(9);
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
describe('itemsPerPeriod parameter', () => {
|
|
799
|
+
it('should limit items per time period', () => {
|
|
800
|
+
// Get periods with limited items
|
|
801
|
+
const periods = db
|
|
802
|
+
.prepare(`
|
|
803
|
+
SELECT DATE(created_at) as period
|
|
804
|
+
FROM context_items
|
|
805
|
+
WHERE session_id = ?
|
|
806
|
+
GROUP BY DATE(created_at)
|
|
807
|
+
ORDER BY period DESC
|
|
808
|
+
`)
|
|
809
|
+
.all(testSessionId);
|
|
810
|
+
const timeline = periods.map(period => {
|
|
811
|
+
const items = db
|
|
812
|
+
.prepare(`
|
|
813
|
+
SELECT * FROM context_items
|
|
814
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
815
|
+
ORDER BY created_at DESC
|
|
816
|
+
LIMIT 2
|
|
817
|
+
`)
|
|
818
|
+
.all(testSessionId, period.period);
|
|
819
|
+
return {
|
|
820
|
+
period: period.period,
|
|
821
|
+
items: items,
|
|
822
|
+
hasMore: db
|
|
823
|
+
.prepare(`
|
|
824
|
+
SELECT COUNT(*) as total FROM context_items
|
|
825
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
826
|
+
`)
|
|
827
|
+
.get(testSessionId, period.period).total > 2,
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
timeline.forEach(period => {
|
|
831
|
+
expect(period.items.length).toBeLessThanOrEqual(2);
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
it('should indicate when more items exist in period', () => {
|
|
835
|
+
// Add many items to today
|
|
836
|
+
const now = new Date();
|
|
837
|
+
for (let i = 0; i < 10; i++) {
|
|
838
|
+
db.prepare(`
|
|
839
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority, created_at)
|
|
840
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
841
|
+
`).run((0, uuid_1.v4)(), testSessionId, `extra_item_${i}`, `Extra value ${i}`, 'note', 'normal', now.toISOString());
|
|
842
|
+
}
|
|
843
|
+
const todayCount = db
|
|
844
|
+
.prepare(`
|
|
845
|
+
SELECT COUNT(*) as count FROM context_items
|
|
846
|
+
WHERE session_id = ? AND DATE(created_at) = DATE('now')
|
|
847
|
+
`)
|
|
848
|
+
.get(testSessionId);
|
|
849
|
+
expect(todayCount.count).toBeGreaterThan(5);
|
|
850
|
+
});
|
|
851
|
+
it('should show most recent items first in each period', () => {
|
|
852
|
+
const periods = db
|
|
853
|
+
.prepare(`
|
|
854
|
+
SELECT DATE(created_at) as period
|
|
855
|
+
FROM context_items
|
|
856
|
+
WHERE session_id = ?
|
|
857
|
+
GROUP BY DATE(created_at)
|
|
858
|
+
`)
|
|
859
|
+
.all(testSessionId);
|
|
860
|
+
periods.forEach(period => {
|
|
861
|
+
const items = db
|
|
862
|
+
.prepare(`
|
|
863
|
+
SELECT * FROM context_items
|
|
864
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
865
|
+
ORDER BY created_at DESC
|
|
866
|
+
`)
|
|
867
|
+
.all(testSessionId, period.period);
|
|
868
|
+
if (items.length > 1) {
|
|
869
|
+
for (let i = 1; i < items.length; i++) {
|
|
870
|
+
const prevTime = new Date(items[i - 1].created_at).getTime();
|
|
871
|
+
const currTime = new Date(items[i].created_at).getTime();
|
|
872
|
+
expect(prevTime).toBeGreaterThanOrEqual(currTime);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
describe('groupBy parameter', () => {
|
|
879
|
+
it('should group by hour', () => {
|
|
880
|
+
const hourlyGroups = db
|
|
881
|
+
.prepare(`
|
|
882
|
+
SELECT
|
|
883
|
+
strftime('%Y-%m-%d %H:00', created_at) as period,
|
|
884
|
+
COUNT(*) as count
|
|
885
|
+
FROM context_items
|
|
886
|
+
WHERE session_id = ?
|
|
887
|
+
GROUP BY strftime('%Y-%m-%d %H:00', created_at)
|
|
888
|
+
ORDER BY period DESC
|
|
889
|
+
`)
|
|
890
|
+
.all(testSessionId);
|
|
891
|
+
expect(hourlyGroups.length).toBeGreaterThan(0);
|
|
892
|
+
expect(hourlyGroups[0].period).toMatch(/\d{4}-\d{2}-\d{2} \d{2}:00/);
|
|
893
|
+
});
|
|
894
|
+
it('should group by day (default)', () => {
|
|
895
|
+
const dailyGroups = db
|
|
896
|
+
.prepare(`
|
|
897
|
+
SELECT
|
|
898
|
+
DATE(created_at) as period,
|
|
899
|
+
COUNT(*) as count
|
|
900
|
+
FROM context_items
|
|
901
|
+
WHERE session_id = ?
|
|
902
|
+
GROUP BY DATE(created_at)
|
|
903
|
+
ORDER BY period DESC
|
|
904
|
+
`)
|
|
905
|
+
.all(testSessionId);
|
|
906
|
+
expect(dailyGroups.length).toBeGreaterThan(0);
|
|
907
|
+
expect(dailyGroups[0].period).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
908
|
+
});
|
|
909
|
+
it('should group by week', () => {
|
|
910
|
+
const weeklyGroups = db
|
|
911
|
+
.prepare(`
|
|
912
|
+
SELECT
|
|
913
|
+
strftime('%Y-W%W', created_at) as period,
|
|
914
|
+
COUNT(*) as count
|
|
915
|
+
FROM context_items
|
|
916
|
+
WHERE session_id = ?
|
|
917
|
+
GROUP BY strftime('%Y-W%W', created_at)
|
|
918
|
+
ORDER BY period DESC
|
|
919
|
+
`)
|
|
920
|
+
.all(testSessionId);
|
|
921
|
+
expect(weeklyGroups.length).toBeGreaterThan(0);
|
|
922
|
+
expect(weeklyGroups[0].period).toMatch(/\d{4}-W\d{2}/);
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
describe('Combining timeline parameters', () => {
|
|
926
|
+
it('should combine categories and date filters', () => {
|
|
927
|
+
// TIMEZONE-SAFE: Use UTC date calculations
|
|
928
|
+
const twoDaysAgo = (0, timezone_safe_dates_1.createUTCDate)(-2);
|
|
929
|
+
const items = db
|
|
930
|
+
.prepare(`
|
|
931
|
+
SELECT * FROM context_items
|
|
932
|
+
WHERE session_id = ?
|
|
933
|
+
AND category IN (?, ?)
|
|
934
|
+
AND created_at >= ?
|
|
935
|
+
ORDER BY created_at DESC
|
|
936
|
+
`)
|
|
937
|
+
.all(testSessionId, 'task', 'note', twoDaysAgo.toISOString());
|
|
938
|
+
expect(items.length).toBeGreaterThan(0);
|
|
939
|
+
expect(items.every(i => ['task', 'note'].includes(i.category))).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
it('should combine includeItems with itemsPerPeriod', () => {
|
|
942
|
+
const periods = db
|
|
943
|
+
.prepare(`
|
|
944
|
+
SELECT DATE(created_at) as period
|
|
945
|
+
FROM context_items
|
|
946
|
+
WHERE session_id = ?
|
|
947
|
+
GROUP BY DATE(created_at)
|
|
948
|
+
ORDER BY period DESC
|
|
949
|
+
`)
|
|
950
|
+
.all(testSessionId);
|
|
951
|
+
const timeline = periods.map(period => {
|
|
952
|
+
const allItems = db
|
|
953
|
+
.prepare(`
|
|
954
|
+
SELECT COUNT(*) as total FROM context_items
|
|
955
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
956
|
+
`)
|
|
957
|
+
.get(testSessionId, period.period);
|
|
958
|
+
const items = db
|
|
959
|
+
.prepare(`
|
|
960
|
+
SELECT * FROM context_items
|
|
961
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
962
|
+
ORDER BY created_at DESC
|
|
963
|
+
LIMIT 3
|
|
964
|
+
`)
|
|
965
|
+
.all(testSessionId, period.period);
|
|
966
|
+
return {
|
|
967
|
+
period: period.period,
|
|
968
|
+
items: items,
|
|
969
|
+
total_count: allItems.total,
|
|
970
|
+
hasMore: allItems.total > 3,
|
|
971
|
+
};
|
|
972
|
+
});
|
|
973
|
+
timeline.forEach(period => {
|
|
974
|
+
expect(period.items.length).toBeLessThanOrEqual(3);
|
|
975
|
+
expect(period.hasMore).toBe(period.total_count > 3);
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
describe('Backward compatibility', () => {
|
|
980
|
+
it('should work with no parameters as before', () => {
|
|
981
|
+
const items = db
|
|
982
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY created_at DESC')
|
|
983
|
+
.all(testSessionId);
|
|
984
|
+
expect(items.length).toBeGreaterThan(0);
|
|
985
|
+
});
|
|
986
|
+
it('should work with only startDate and endDate as before', () => {
|
|
987
|
+
// TIMEZONE-SAFE: Use UTC date range
|
|
988
|
+
const { today: _today } = (0, timezone_safe_dates_1.createTimelineTestDates)();
|
|
989
|
+
const startDate = (0, timezone_safe_dates_1.createUTCDate)(-7);
|
|
990
|
+
const endDate = _today;
|
|
991
|
+
const items = db
|
|
992
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ? AND created_at <= ?')
|
|
993
|
+
.all(testSessionId, startDate.toISOString(), endDate.toISOString());
|
|
994
|
+
expect(items.length).toBeGreaterThan(0);
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
describe('Edge cases', () => {
|
|
998
|
+
it('should handle empty timeline gracefully', () => {
|
|
999
|
+
const emptySessionId = (0, uuid_1.v4)();
|
|
1000
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
|
|
1001
|
+
const items = db
|
|
1002
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
1003
|
+
.all(emptySessionId);
|
|
1004
|
+
expect(items).toHaveLength(0);
|
|
1005
|
+
});
|
|
1006
|
+
it('should handle future dates in relativeTime', () => {
|
|
1007
|
+
const tomorrow = new Date();
|
|
1008
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
1009
|
+
const items = db
|
|
1010
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at >= ?')
|
|
1011
|
+
.all(testSessionId, tomorrow.toISOString());
|
|
1012
|
+
expect(items).toHaveLength(0);
|
|
1013
|
+
});
|
|
1014
|
+
it('should handle very large itemsPerPeriod values', () => {
|
|
1015
|
+
const periods = db
|
|
1016
|
+
.prepare(`
|
|
1017
|
+
SELECT DATE(created_at) as period
|
|
1018
|
+
FROM context_items
|
|
1019
|
+
WHERE session_id = ?
|
|
1020
|
+
GROUP BY DATE(created_at)
|
|
1021
|
+
`)
|
|
1022
|
+
.all(testSessionId);
|
|
1023
|
+
periods.forEach(period => {
|
|
1024
|
+
const items = db
|
|
1025
|
+
.prepare(`
|
|
1026
|
+
SELECT * FROM context_items
|
|
1027
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
1028
|
+
ORDER BY created_at DESC
|
|
1029
|
+
LIMIT 99999
|
|
1030
|
+
`)
|
|
1031
|
+
.all(testSessionId, period.period);
|
|
1032
|
+
const totalCount = db
|
|
1033
|
+
.prepare(`
|
|
1034
|
+
SELECT COUNT(*) as count FROM context_items
|
|
1035
|
+
WHERE session_id = ? AND DATE(created_at) = ?
|
|
1036
|
+
`)
|
|
1037
|
+
.get(testSessionId, period.period);
|
|
1038
|
+
expect(items).toHaveLength(totalCount.count);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
describe('Performance considerations', () => {
|
|
1044
|
+
it('should handle large datasets efficiently', () => {
|
|
1045
|
+
// Add many items
|
|
1046
|
+
const startTime = Date.now();
|
|
1047
|
+
db.transaction(() => {
|
|
1048
|
+
for (let i = 0; i < 1000; i++) {
|
|
1049
|
+
db.prepare(`
|
|
1050
|
+
INSERT INTO context_items (id, session_id, key, value, category, priority)
|
|
1051
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1052
|
+
`).run((0, uuid_1.v4)(), testSessionId, `perf_test_${i}`, `Performance test value ${i}`, i % 2 === 0 ? 'task' : 'note', i % 3 === 0 ? 'high' : 'normal');
|
|
1053
|
+
}
|
|
1054
|
+
})();
|
|
1055
|
+
const insertTime = Date.now() - startTime;
|
|
1056
|
+
expect(insertTime).toBeLessThan(1000); // Should complete within 1 second
|
|
1057
|
+
// Test query performance
|
|
1058
|
+
const queryStartTime = Date.now();
|
|
1059
|
+
const items = db
|
|
1060
|
+
.prepare(`
|
|
1061
|
+
SELECT * FROM context_items
|
|
1062
|
+
WHERE session_id = ?
|
|
1063
|
+
AND category = ?
|
|
1064
|
+
AND priority = ?
|
|
1065
|
+
ORDER BY created_at DESC
|
|
1066
|
+
LIMIT 50 OFFSET 100
|
|
1067
|
+
`)
|
|
1068
|
+
.all(testSessionId, 'task', 'high');
|
|
1069
|
+
const queryTime = Date.now() - queryStartTime;
|
|
1070
|
+
expect(queryTime).toBeLessThan(100); // Should complete within 100ms
|
|
1071
|
+
expect(items.length).toBeLessThanOrEqual(50);
|
|
1072
|
+
});
|
|
1073
|
+
it('should use indexes effectively', () => {
|
|
1074
|
+
// Check that indexes exist
|
|
1075
|
+
const indexes = db
|
|
1076
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'context_items'")
|
|
1077
|
+
.all();
|
|
1078
|
+
expect(indexes.length).toBeGreaterThan(0);
|
|
1079
|
+
expect(indexes.some(idx => idx.name.includes('session'))).toBe(true);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
});
|