@smallironman/mcp-memory-keeper 0.12.2-fork1
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 +542 -0
- package/LICENSE +21 -0
- package/README.md +1281 -0
- package/bin/mcp-memory-keeper +54 -0
- package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
- package/dist/__tests__/e2e/server-e2e.test.js +341 -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 +1054 -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/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 +411 -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/filterBySessionId.test.js +251 -0
- package/dist/__tests__/integration/git-integration.test.js +241 -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/issue24-final-fix.test.js +241 -0
- package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
- package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
- package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
- package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -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 +291 -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 +305 -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/tokenLimitEnforcement.test.js +134 -0
- package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -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 +192 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/token-limits.test.js +225 -0
- package/dist/__tests__/utils/tool-profiles.test.js +374 -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 +4425 -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 +2017 -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/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 +780 -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/token-limits.js +350 -0
- package/dist/utils/tool-profiles.js +242 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/examples/config.json +31 -0
- package/examples/project-directory-setup.md +114 -0
- package/package.json +85 -0
|
@@ -0,0 +1,1054 @@
|
|
|
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 ContextRepository_1 = require("../../repositories/ContextRepository");
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const uuid_1 = require("uuid");
|
|
42
|
+
describe('Enhanced Context Search Integration Tests', () => {
|
|
43
|
+
let dbManager;
|
|
44
|
+
let tempDbPath;
|
|
45
|
+
let db;
|
|
46
|
+
let testSessionId;
|
|
47
|
+
let otherSessionId;
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
tempDbPath = path.join(os.tmpdir(), `test-context-search-${Date.now()}.db`);
|
|
50
|
+
dbManager = new database_1.DatabaseManager({
|
|
51
|
+
filename: tempDbPath,
|
|
52
|
+
maxSize: 10 * 1024 * 1024,
|
|
53
|
+
walMode: true,
|
|
54
|
+
});
|
|
55
|
+
db = dbManager.getDatabase();
|
|
56
|
+
// Create test sessions
|
|
57
|
+
testSessionId = (0, uuid_1.v4)();
|
|
58
|
+
otherSessionId = (0, uuid_1.v4)();
|
|
59
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Main Test Session');
|
|
60
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
|
|
61
|
+
});
|
|
62
|
+
afterEach(() => {
|
|
63
|
+
dbManager.close();
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(tempDbPath);
|
|
66
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
67
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
68
|
+
}
|
|
69
|
+
catch (_e) {
|
|
70
|
+
// Ignore
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
describe('Backward Compatibility', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Add test data
|
|
76
|
+
const items = [
|
|
77
|
+
{
|
|
78
|
+
key: 'auth_config',
|
|
79
|
+
value: 'Authentication configuration settings',
|
|
80
|
+
category: 'config',
|
|
81
|
+
priority: 'high',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: 'db_connection',
|
|
85
|
+
value: 'Database connection string for auth',
|
|
86
|
+
category: 'config',
|
|
87
|
+
priority: 'normal',
|
|
88
|
+
},
|
|
89
|
+
{ key: 'user_model', value: 'User model definition', category: 'code', priority: 'high' },
|
|
90
|
+
{
|
|
91
|
+
key: 'auth_middleware',
|
|
92
|
+
value: 'Authentication middleware implementation',
|
|
93
|
+
category: 'code',
|
|
94
|
+
priority: 'normal',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
items.forEach(item => {
|
|
98
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
it('should maintain existing search functionality with query parameter', () => {
|
|
102
|
+
// Test existing simple search - should search in both key and value
|
|
103
|
+
const sql = `
|
|
104
|
+
SELECT * FROM context_items
|
|
105
|
+
WHERE session_id = ? AND (key LIKE ? OR value LIKE ?)
|
|
106
|
+
ORDER BY priority DESC, created_at DESC
|
|
107
|
+
`;
|
|
108
|
+
const results = db.prepare(sql).all(testSessionId, '%auth%', '%auth%');
|
|
109
|
+
expect(results).toHaveLength(3); // auth_config, auth_middleware, db_connection (has 'auth' in value)
|
|
110
|
+
expect(results.map((r) => r.key)).toContain('auth_config');
|
|
111
|
+
expect(results.map((r) => r.key)).toContain('auth_middleware');
|
|
112
|
+
expect(results.map((r) => r.key)).toContain('db_connection');
|
|
113
|
+
});
|
|
114
|
+
it('should support searchIn parameter for backward compatibility', () => {
|
|
115
|
+
// Search only in keys
|
|
116
|
+
const keyResults = db
|
|
117
|
+
.prepare(`
|
|
118
|
+
SELECT * FROM context_items
|
|
119
|
+
WHERE session_id = ? AND key LIKE ?
|
|
120
|
+
ORDER BY priority DESC, created_at DESC
|
|
121
|
+
`)
|
|
122
|
+
.all(testSessionId, '%auth%');
|
|
123
|
+
expect(keyResults).toHaveLength(2); // auth_config, auth_middleware
|
|
124
|
+
// Search only in values
|
|
125
|
+
const valueResults = db
|
|
126
|
+
.prepare(`
|
|
127
|
+
SELECT * FROM context_items
|
|
128
|
+
WHERE session_id = ? AND value LIKE ?
|
|
129
|
+
ORDER BY priority DESC, created_at DESC
|
|
130
|
+
`)
|
|
131
|
+
.all(testSessionId, '%auth%');
|
|
132
|
+
expect(valueResults).toHaveLength(3); // auth_config, auth_middleware, db_connection
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Time Filtering', () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
const now = new Date();
|
|
138
|
+
const items = [
|
|
139
|
+
{ key: 'today_item', value: 'Created today', created_at: now.toISOString() },
|
|
140
|
+
{
|
|
141
|
+
key: 'yesterday_item',
|
|
142
|
+
value: 'Created yesterday',
|
|
143
|
+
created_at: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
key: 'two_days_ago',
|
|
147
|
+
value: 'Created 2 days ago',
|
|
148
|
+
created_at: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: 'week_old',
|
|
152
|
+
value: 'Created a week ago',
|
|
153
|
+
created_at: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
key: 'month_old',
|
|
157
|
+
value: 'Created a month ago',
|
|
158
|
+
created_at: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
items.forEach(item => {
|
|
162
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.created_at);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
it('should filter by createdAfter', () => {
|
|
166
|
+
const threeDaysAgo = new Date();
|
|
167
|
+
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
|
|
168
|
+
const results = db
|
|
169
|
+
.prepare(`
|
|
170
|
+
SELECT * FROM context_items
|
|
171
|
+
WHERE session_id = ? AND created_at > ?
|
|
172
|
+
ORDER BY created_at DESC
|
|
173
|
+
`)
|
|
174
|
+
.all(testSessionId, threeDaysAgo.toISOString());
|
|
175
|
+
expect(results).toHaveLength(3); // today, yesterday, two_days_ago
|
|
176
|
+
expect(results.map((r) => r.key)).toContain('today_item');
|
|
177
|
+
expect(results.map((r) => r.key)).toContain('yesterday_item');
|
|
178
|
+
expect(results.map((r) => r.key)).toContain('two_days_ago');
|
|
179
|
+
});
|
|
180
|
+
it('should filter by createdBefore', () => {
|
|
181
|
+
// Get all items first to check their timestamps
|
|
182
|
+
const allItems = db
|
|
183
|
+
.prepare(`
|
|
184
|
+
SELECT * FROM context_items
|
|
185
|
+
WHERE session_id = ?
|
|
186
|
+
ORDER BY created_at DESC
|
|
187
|
+
`)
|
|
188
|
+
.all(testSessionId);
|
|
189
|
+
// Find the two_days_ago item to get its exact timestamp
|
|
190
|
+
const twoDaysAgoItem = allItems.find((item) => item.key === 'two_days_ago');
|
|
191
|
+
// Use a timestamp just after the two_days_ago item to exclude it
|
|
192
|
+
const cutoffDate = new Date(twoDaysAgoItem.created_at);
|
|
193
|
+
cutoffDate.setMilliseconds(cutoffDate.getMilliseconds() - 1); // Go back 1ms to exclude this item
|
|
194
|
+
const results = db
|
|
195
|
+
.prepare(`
|
|
196
|
+
SELECT * FROM context_items
|
|
197
|
+
WHERE session_id = ? AND created_at < ?
|
|
198
|
+
ORDER BY created_at DESC
|
|
199
|
+
`)
|
|
200
|
+
.all(testSessionId, cutoffDate.toISOString());
|
|
201
|
+
expect(results).toHaveLength(2); // week_old, month_old
|
|
202
|
+
expect(results.map((r) => r.key)).toContain('week_old');
|
|
203
|
+
expect(results.map((r) => r.key)).toContain('month_old');
|
|
204
|
+
expect(results.map((r) => r.key)).not.toContain('two_days_ago');
|
|
205
|
+
});
|
|
206
|
+
it('should support relative time parsing', () => {
|
|
207
|
+
// Test "2 hours ago"
|
|
208
|
+
const twoHoursAgo = new Date();
|
|
209
|
+
twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
|
|
210
|
+
// Add an item from 1 hour ago
|
|
211
|
+
const oneHourAgo = new Date();
|
|
212
|
+
oneHourAgo.setHours(oneHourAgo.getHours() - 1);
|
|
213
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'recent_item', 'Created 1 hour ago', oneHourAgo.toISOString());
|
|
214
|
+
const results = db
|
|
215
|
+
.prepare(`
|
|
216
|
+
SELECT * FROM context_items
|
|
217
|
+
WHERE session_id = ? AND created_at > ?
|
|
218
|
+
ORDER BY created_at DESC
|
|
219
|
+
`)
|
|
220
|
+
.all(testSessionId, twoHoursAgo.toISOString());
|
|
221
|
+
const recentResults = results.filter((r) => new Date(r.created_at).getTime() > twoHoursAgo.getTime());
|
|
222
|
+
expect(recentResults.some((r) => r.key === 'recent_item')).toBe(true);
|
|
223
|
+
expect(recentResults.some((r) => r.key === 'today_item')).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
it('should handle "yesterday" as relative time', () => {
|
|
226
|
+
const now = new Date();
|
|
227
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
228
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
229
|
+
const results = db
|
|
230
|
+
.prepare(`
|
|
231
|
+
SELECT * FROM context_items
|
|
232
|
+
WHERE session_id = ?
|
|
233
|
+
AND created_at >= ?
|
|
234
|
+
AND created_at < ?
|
|
235
|
+
ORDER BY created_at DESC
|
|
236
|
+
`)
|
|
237
|
+
.all(testSessionId, yesterday.toISOString(), today.toISOString());
|
|
238
|
+
// Should only include yesterday_item
|
|
239
|
+
const yesterdayResults = results.filter((r) => {
|
|
240
|
+
const itemDate = new Date(r.created_at);
|
|
241
|
+
return itemDate >= yesterday && itemDate < today;
|
|
242
|
+
});
|
|
243
|
+
expect(yesterdayResults.length).toBeGreaterThan(0);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('Channel Filtering', () => {
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
const items = [
|
|
249
|
+
{ key: 'main_task', value: 'Main channel task', channel: 'main', priority: 'high' },
|
|
250
|
+
{
|
|
251
|
+
key: 'feature_task',
|
|
252
|
+
value: 'Feature branch task',
|
|
253
|
+
channel: 'feature/auth',
|
|
254
|
+
priority: 'normal',
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
key: 'feature_bug',
|
|
258
|
+
value: 'Feature branch bug',
|
|
259
|
+
channel: 'feature/auth',
|
|
260
|
+
priority: 'high',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: 'hotfix_task',
|
|
264
|
+
value: 'Hotfix task',
|
|
265
|
+
channel: 'hotfix/security',
|
|
266
|
+
priority: 'critical',
|
|
267
|
+
},
|
|
268
|
+
{ key: 'no_channel', value: 'Task without channel', channel: null, priority: 'normal' },
|
|
269
|
+
];
|
|
270
|
+
items.forEach(item => {
|
|
271
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.channel, item.priority);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it('should filter by single channel', () => {
|
|
275
|
+
const results = db
|
|
276
|
+
.prepare(`
|
|
277
|
+
SELECT * FROM context_items
|
|
278
|
+
WHERE session_id = ? AND channel = ?
|
|
279
|
+
ORDER BY priority DESC, created_at DESC
|
|
280
|
+
`)
|
|
281
|
+
.all(testSessionId, 'feature/auth');
|
|
282
|
+
expect(results).toHaveLength(2);
|
|
283
|
+
expect(results.map((r) => r.key)).toContain('feature_task');
|
|
284
|
+
expect(results.map((r) => r.key)).toContain('feature_bug');
|
|
285
|
+
});
|
|
286
|
+
it('should filter by multiple channels', () => {
|
|
287
|
+
const channels = ['main', 'hotfix/security'];
|
|
288
|
+
const placeholders = channels.map(() => '?').join(',');
|
|
289
|
+
const results = db
|
|
290
|
+
.prepare(`
|
|
291
|
+
SELECT * FROM context_items
|
|
292
|
+
WHERE session_id = ? AND channel IN (${placeholders})
|
|
293
|
+
ORDER BY priority DESC, created_at DESC
|
|
294
|
+
`)
|
|
295
|
+
.all(testSessionId, ...channels);
|
|
296
|
+
expect(results).toHaveLength(2);
|
|
297
|
+
expect(results.map((r) => r.key)).toContain('main_task');
|
|
298
|
+
expect(results.map((r) => r.key)).toContain('hotfix_task');
|
|
299
|
+
});
|
|
300
|
+
it('should handle items without channels', () => {
|
|
301
|
+
const results = db
|
|
302
|
+
.prepare(`
|
|
303
|
+
SELECT * FROM context_items
|
|
304
|
+
WHERE session_id = ? AND channel IS NULL
|
|
305
|
+
ORDER BY created_at DESC
|
|
306
|
+
`)
|
|
307
|
+
.all(testSessionId);
|
|
308
|
+
expect(results).toHaveLength(1);
|
|
309
|
+
expect(results[0].key).toBe('no_channel');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
describe('Sort Options', () => {
|
|
313
|
+
beforeEach(() => {
|
|
314
|
+
// Create items with specific timestamps and keys for testing sort
|
|
315
|
+
const baseTime = new Date('2024-01-01T00:00:00Z');
|
|
316
|
+
const items = [
|
|
317
|
+
{
|
|
318
|
+
key: 'alpha_item',
|
|
319
|
+
value: 'First alphabetically',
|
|
320
|
+
created_at: new Date(baseTime.getTime() + 1000).toISOString(),
|
|
321
|
+
updated_at: new Date(baseTime.getTime() + 5000).toISOString(),
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
key: 'beta_item',
|
|
325
|
+
value: 'Second alphabetically',
|
|
326
|
+
created_at: new Date(baseTime.getTime() + 2000).toISOString(),
|
|
327
|
+
updated_at: new Date(baseTime.getTime() + 4000).toISOString(),
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
key: 'charlie_item',
|
|
331
|
+
value: 'Third alphabetically',
|
|
332
|
+
created_at: new Date(baseTime.getTime() + 3000).toISOString(),
|
|
333
|
+
updated_at: new Date(baseTime.getTime() + 3000).toISOString(),
|
|
334
|
+
},
|
|
335
|
+
];
|
|
336
|
+
items.forEach(item => {
|
|
337
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.created_at, item.updated_at);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
it('should sort by created_at descending (default)', () => {
|
|
341
|
+
const results = db
|
|
342
|
+
.prepare(`
|
|
343
|
+
SELECT * FROM context_items
|
|
344
|
+
WHERE session_id = ?
|
|
345
|
+
ORDER BY created_at DESC
|
|
346
|
+
`)
|
|
347
|
+
.all(testSessionId);
|
|
348
|
+
expect(results[0].key).toBe('charlie_item'); // Most recent
|
|
349
|
+
expect(results[results.length - 1].key).toBe('alpha_item'); // Oldest
|
|
350
|
+
});
|
|
351
|
+
it('should sort by created_at ascending', () => {
|
|
352
|
+
const results = db
|
|
353
|
+
.prepare(`
|
|
354
|
+
SELECT * FROM context_items
|
|
355
|
+
WHERE session_id = ?
|
|
356
|
+
ORDER BY created_at ASC
|
|
357
|
+
`)
|
|
358
|
+
.all(testSessionId);
|
|
359
|
+
expect(results[0].key).toBe('alpha_item'); // Oldest
|
|
360
|
+
expect(results[results.length - 1].key).toBe('charlie_item'); // Most recent
|
|
361
|
+
});
|
|
362
|
+
it('should sort by updated_at descending', () => {
|
|
363
|
+
const results = db
|
|
364
|
+
.prepare(`
|
|
365
|
+
SELECT * FROM context_items
|
|
366
|
+
WHERE session_id = ?
|
|
367
|
+
ORDER BY updated_at DESC
|
|
368
|
+
`)
|
|
369
|
+
.all(testSessionId);
|
|
370
|
+
expect(results[0].key).toBe('alpha_item'); // Most recently updated
|
|
371
|
+
expect(results[results.length - 1].key).toBe('charlie_item'); // Least recently updated
|
|
372
|
+
});
|
|
373
|
+
it('should sort by key ascending', () => {
|
|
374
|
+
const results = db
|
|
375
|
+
.prepare(`
|
|
376
|
+
SELECT * FROM context_items
|
|
377
|
+
WHERE session_id = ?
|
|
378
|
+
ORDER BY key ASC
|
|
379
|
+
`)
|
|
380
|
+
.all(testSessionId);
|
|
381
|
+
expect(results[0].key).toBe('alpha_item');
|
|
382
|
+
expect(results[1].key).toBe('beta_item');
|
|
383
|
+
expect(results[2].key).toBe('charlie_item');
|
|
384
|
+
});
|
|
385
|
+
it('should sort by key descending', () => {
|
|
386
|
+
const results = db
|
|
387
|
+
.prepare(`
|
|
388
|
+
SELECT * FROM context_items
|
|
389
|
+
WHERE session_id = ?
|
|
390
|
+
ORDER BY key DESC
|
|
391
|
+
`)
|
|
392
|
+
.all(testSessionId);
|
|
393
|
+
expect(results[0].key).toBe('charlie_item');
|
|
394
|
+
expect(results[1].key).toBe('beta_item');
|
|
395
|
+
expect(results[2].key).toBe('alpha_item');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
describe('Metadata and Size', () => {
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
const items = [
|
|
401
|
+
{
|
|
402
|
+
key: 'small_item',
|
|
403
|
+
value: 'Small content',
|
|
404
|
+
metadata: JSON.stringify({ tags: ['small', 'test'] }),
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
key: 'large_item',
|
|
408
|
+
value: 'A'.repeat(1000), // Large content
|
|
409
|
+
metadata: JSON.stringify({ tags: ['large', 'performance'] }),
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
key: 'no_metadata',
|
|
413
|
+
value: 'Item without metadata',
|
|
414
|
+
metadata: null,
|
|
415
|
+
},
|
|
416
|
+
];
|
|
417
|
+
items.forEach(item => {
|
|
418
|
+
const size = Buffer.byteLength(item.value, 'utf8');
|
|
419
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, metadata, size) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.metadata, size);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
it('should include metadata when requested', () => {
|
|
423
|
+
const results = db
|
|
424
|
+
.prepare(`
|
|
425
|
+
SELECT *, size FROM context_items
|
|
426
|
+
WHERE session_id = ?
|
|
427
|
+
`)
|
|
428
|
+
.all(testSessionId);
|
|
429
|
+
results.forEach((item) => {
|
|
430
|
+
if (item.metadata) {
|
|
431
|
+
const parsed = JSON.parse(item.metadata);
|
|
432
|
+
expect(parsed).toHaveProperty('tags');
|
|
433
|
+
expect(Array.isArray(parsed.tags)).toBe(true);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
it('should include size information', () => {
|
|
438
|
+
const results = db
|
|
439
|
+
.prepare(`
|
|
440
|
+
SELECT *, size FROM context_items
|
|
441
|
+
WHERE session_id = ?
|
|
442
|
+
`)
|
|
443
|
+
.all(testSessionId);
|
|
444
|
+
const smallItem = results.find((r) => r.key === 'small_item');
|
|
445
|
+
const largeItem = results.find((r) => r.key === 'large_item');
|
|
446
|
+
expect(smallItem.size).toBeLessThan(100);
|
|
447
|
+
expect(largeItem.size).toBeGreaterThan(900);
|
|
448
|
+
});
|
|
449
|
+
it('should calculate size if not stored', () => {
|
|
450
|
+
// For items without stored size, it should be calculated
|
|
451
|
+
const results = db
|
|
452
|
+
.prepare(`
|
|
453
|
+
SELECT *,
|
|
454
|
+
CASE
|
|
455
|
+
WHEN size IS NULL THEN LENGTH(value)
|
|
456
|
+
ELSE size
|
|
457
|
+
END as calculated_size
|
|
458
|
+
FROM context_items
|
|
459
|
+
WHERE session_id = ?
|
|
460
|
+
`)
|
|
461
|
+
.all(testSessionId);
|
|
462
|
+
results.forEach((item) => {
|
|
463
|
+
expect(item.calculated_size).toBeGreaterThan(0);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
describe('Pagination', () => {
|
|
468
|
+
beforeEach(() => {
|
|
469
|
+
// Create 50 items for pagination testing
|
|
470
|
+
for (let i = 0; i < 50; i++) {
|
|
471
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, priority) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(2, '0')}`, `Value for item ${i}`, i % 3 === 0 ? 'high' : 'normal');
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
it('should limit results', () => {
|
|
475
|
+
const results = db
|
|
476
|
+
.prepare(`
|
|
477
|
+
SELECT * FROM context_items
|
|
478
|
+
WHERE session_id = ?
|
|
479
|
+
ORDER BY key ASC
|
|
480
|
+
LIMIT 10
|
|
481
|
+
`)
|
|
482
|
+
.all(testSessionId);
|
|
483
|
+
expect(results).toHaveLength(10);
|
|
484
|
+
expect(results[0].key).toBe('item_00');
|
|
485
|
+
expect(results[9].key).toBe('item_09');
|
|
486
|
+
});
|
|
487
|
+
it('should support offset for pagination', () => {
|
|
488
|
+
const results = db
|
|
489
|
+
.prepare(`
|
|
490
|
+
SELECT * FROM context_items
|
|
491
|
+
WHERE session_id = ?
|
|
492
|
+
ORDER BY key ASC
|
|
493
|
+
LIMIT 10 OFFSET 20
|
|
494
|
+
`)
|
|
495
|
+
.all(testSessionId);
|
|
496
|
+
expect(results).toHaveLength(10);
|
|
497
|
+
expect(results[0].key).toBe('item_20');
|
|
498
|
+
expect(results[9].key).toBe('item_29');
|
|
499
|
+
});
|
|
500
|
+
it('should return total count for pagination', () => {
|
|
501
|
+
// First get total count
|
|
502
|
+
const countResult = db
|
|
503
|
+
.prepare(`
|
|
504
|
+
SELECT COUNT(*) as count FROM context_items
|
|
505
|
+
WHERE session_id = ?
|
|
506
|
+
`)
|
|
507
|
+
.get(testSessionId);
|
|
508
|
+
expect(countResult.count).toBe(50);
|
|
509
|
+
// Then get paginated results
|
|
510
|
+
const results = db
|
|
511
|
+
.prepare(`
|
|
512
|
+
SELECT * FROM context_items
|
|
513
|
+
WHERE session_id = ?
|
|
514
|
+
ORDER BY key ASC
|
|
515
|
+
LIMIT 10 OFFSET 10
|
|
516
|
+
`)
|
|
517
|
+
.all(testSessionId);
|
|
518
|
+
expect(results).toHaveLength(10);
|
|
519
|
+
expect(results[0].key).toBe('item_10');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
describe('Key Pattern Matching', () => {
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
const items = [
|
|
525
|
+
{ key: 'auth_login', value: 'Login functionality' },
|
|
526
|
+
{ key: 'auth_logout', value: 'Logout functionality' },
|
|
527
|
+
{ key: 'auth_register', value: 'Registration functionality' },
|
|
528
|
+
{ key: 'user_profile', value: 'User profile page' },
|
|
529
|
+
{ key: 'user_settings', value: 'User settings page' },
|
|
530
|
+
{ key: 'admin_dashboard', value: 'Admin dashboard' },
|
|
531
|
+
{ key: 'test_auth_unit', value: 'Unit tests for auth' },
|
|
532
|
+
];
|
|
533
|
+
items.forEach(item => {
|
|
534
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
it('should match keys with glob pattern', () => {
|
|
538
|
+
// SQLite GLOB pattern for keys starting with 'auth_'
|
|
539
|
+
const results = db
|
|
540
|
+
.prepare(`
|
|
541
|
+
SELECT * FROM context_items
|
|
542
|
+
WHERE session_id = ? AND key GLOB 'auth_*'
|
|
543
|
+
ORDER BY key ASC
|
|
544
|
+
`)
|
|
545
|
+
.all(testSessionId);
|
|
546
|
+
expect(results).toHaveLength(3);
|
|
547
|
+
expect(results.map((r) => r.key)).toEqual([
|
|
548
|
+
'auth_login',
|
|
549
|
+
'auth_logout',
|
|
550
|
+
'auth_register',
|
|
551
|
+
]);
|
|
552
|
+
});
|
|
553
|
+
it('should match keys ending with pattern', () => {
|
|
554
|
+
// Keys ending with '_settings'
|
|
555
|
+
const results = db
|
|
556
|
+
.prepare(`
|
|
557
|
+
SELECT * FROM context_items
|
|
558
|
+
WHERE session_id = ? AND key GLOB '*_settings'
|
|
559
|
+
ORDER BY key ASC
|
|
560
|
+
`)
|
|
561
|
+
.all(testSessionId);
|
|
562
|
+
expect(results).toHaveLength(1);
|
|
563
|
+
expect(results[0].key).toBe('user_settings');
|
|
564
|
+
});
|
|
565
|
+
it('should match keys containing pattern', () => {
|
|
566
|
+
// Keys containing 'auth'
|
|
567
|
+
const results = db
|
|
568
|
+
.prepare(`
|
|
569
|
+
SELECT * FROM context_items
|
|
570
|
+
WHERE session_id = ? AND key GLOB '*auth*'
|
|
571
|
+
ORDER BY key ASC
|
|
572
|
+
`)
|
|
573
|
+
.all(testSessionId);
|
|
574
|
+
expect(results).toHaveLength(4); // auth_login, auth_logout, auth_register, test_auth_unit
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
describe('Priority Filtering', () => {
|
|
578
|
+
beforeEach(() => {
|
|
579
|
+
const items = [
|
|
580
|
+
{ key: 'critical_bug', value: 'Critical security issue', priority: 'critical' },
|
|
581
|
+
{ key: 'high_task1', value: 'Important feature', priority: 'high' },
|
|
582
|
+
{ key: 'high_task2', value: 'Important bugfix', priority: 'high' },
|
|
583
|
+
{ key: 'normal_task1', value: 'Regular task', priority: 'normal' },
|
|
584
|
+
{ key: 'normal_task2', value: 'Another regular task', priority: 'normal' },
|
|
585
|
+
{ key: 'low_task', value: 'Nice to have', priority: 'low' },
|
|
586
|
+
];
|
|
587
|
+
items.forEach(item => {
|
|
588
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, priority) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.priority);
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
it('should filter by single priority', () => {
|
|
592
|
+
const results = db
|
|
593
|
+
.prepare(`
|
|
594
|
+
SELECT * FROM context_items
|
|
595
|
+
WHERE session_id = ? AND priority = ?
|
|
596
|
+
ORDER BY created_at DESC
|
|
597
|
+
`)
|
|
598
|
+
.all(testSessionId, 'high');
|
|
599
|
+
expect(results).toHaveLength(2);
|
|
600
|
+
expect(results.map((r) => r.key)).toContain('high_task1');
|
|
601
|
+
expect(results.map((r) => r.key)).toContain('high_task2');
|
|
602
|
+
});
|
|
603
|
+
it('should filter by multiple priorities', () => {
|
|
604
|
+
const priorities = ['critical', 'high'];
|
|
605
|
+
const placeholders = priorities.map(() => '?').join(',');
|
|
606
|
+
const results = db
|
|
607
|
+
.prepare(`
|
|
608
|
+
SELECT * FROM context_items
|
|
609
|
+
WHERE session_id = ? AND priority IN (${placeholders})
|
|
610
|
+
ORDER BY
|
|
611
|
+
CASE priority
|
|
612
|
+
WHEN 'critical' THEN 1
|
|
613
|
+
WHEN 'high' THEN 2
|
|
614
|
+
WHEN 'normal' THEN 3
|
|
615
|
+
WHEN 'low' THEN 4
|
|
616
|
+
END
|
|
617
|
+
`)
|
|
618
|
+
.all(testSessionId, ...priorities);
|
|
619
|
+
expect(results).toHaveLength(3);
|
|
620
|
+
expect(results[0].key).toBe('critical_bug'); // Critical comes first
|
|
621
|
+
expect(results.slice(1).map((r) => r.priority)).toEqual(['high', 'high']);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
describe('Privacy and Session Boundaries', () => {
|
|
625
|
+
beforeEach(() => {
|
|
626
|
+
// Add items to main session
|
|
627
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'public_item', 'Public content', 0);
|
|
628
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'private_item', 'Private content', 1);
|
|
629
|
+
// Add items to other session
|
|
630
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_public', 'Other public content', 0);
|
|
631
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_private', 'Other private content', 1);
|
|
632
|
+
});
|
|
633
|
+
it('should only show private items to owner session', () => {
|
|
634
|
+
// Search from main session - should see own private items
|
|
635
|
+
const mainResults = db
|
|
636
|
+
.prepare(`
|
|
637
|
+
SELECT * FROM context_items
|
|
638
|
+
WHERE (key LIKE ? OR value LIKE ?)
|
|
639
|
+
AND (is_private = 0 OR session_id = ?)
|
|
640
|
+
ORDER BY created_at DESC
|
|
641
|
+
`)
|
|
642
|
+
.all('%content%', '%content%', testSessionId);
|
|
643
|
+
expect(mainResults.map((r) => r.key)).toContain('public_item');
|
|
644
|
+
expect(mainResults.map((r) => r.key)).toContain('private_item');
|
|
645
|
+
expect(mainResults.map((r) => r.key)).toContain('other_public');
|
|
646
|
+
expect(mainResults.map((r) => r.key)).not.toContain('other_private');
|
|
647
|
+
});
|
|
648
|
+
it('should not show other sessions private items', () => {
|
|
649
|
+
// Search from other session - should not see main session's private items
|
|
650
|
+
const otherResults = db
|
|
651
|
+
.prepare(`
|
|
652
|
+
SELECT * FROM context_items
|
|
653
|
+
WHERE (key LIKE ? OR value LIKE ?)
|
|
654
|
+
AND (is_private = 0 OR session_id = ?)
|
|
655
|
+
ORDER BY created_at DESC
|
|
656
|
+
`)
|
|
657
|
+
.all('%content%', '%content%', otherSessionId);
|
|
658
|
+
expect(otherResults.map((r) => r.key)).toContain('public_item');
|
|
659
|
+
expect(otherResults.map((r) => r.key)).not.toContain('private_item');
|
|
660
|
+
expect(otherResults.map((r) => r.key)).toContain('other_public');
|
|
661
|
+
expect(otherResults.map((r) => r.key)).toContain('other_private');
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
describe('Combined Filters', () => {
|
|
665
|
+
beforeEach(() => {
|
|
666
|
+
const now = new Date();
|
|
667
|
+
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
668
|
+
const items = [
|
|
669
|
+
{
|
|
670
|
+
key: 'auth_recent_high',
|
|
671
|
+
value: 'Recent high priority auth task',
|
|
672
|
+
category: 'task',
|
|
673
|
+
priority: 'high',
|
|
674
|
+
channel: 'feature/auth',
|
|
675
|
+
created_at: now.toISOString(),
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
key: 'auth_old_normal',
|
|
679
|
+
value: 'Old normal priority auth task',
|
|
680
|
+
category: 'task',
|
|
681
|
+
priority: 'normal',
|
|
682
|
+
channel: 'feature/auth',
|
|
683
|
+
created_at: yesterday.toISOString(),
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
key: 'db_recent_high',
|
|
687
|
+
value: 'Recent high priority database task',
|
|
688
|
+
category: 'task',
|
|
689
|
+
priority: 'high',
|
|
690
|
+
channel: 'main',
|
|
691
|
+
created_at: now.toISOString(),
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
key: 'ui_recent_normal',
|
|
695
|
+
value: 'Recent normal priority UI task',
|
|
696
|
+
category: 'task',
|
|
697
|
+
priority: 'normal',
|
|
698
|
+
channel: 'feature/ui',
|
|
699
|
+
created_at: now.toISOString(),
|
|
700
|
+
},
|
|
701
|
+
];
|
|
702
|
+
items.forEach(item => {
|
|
703
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, channel, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, item.channel, item.created_at);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
it('should combine search query with time and priority filters', () => {
|
|
707
|
+
const oneDayAgo = new Date();
|
|
708
|
+
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
|
709
|
+
oneDayAgo.setHours(oneDayAgo.getHours() - 1); // A bit more than 24 hours ago
|
|
710
|
+
const results = db
|
|
711
|
+
.prepare(`
|
|
712
|
+
SELECT * FROM context_items
|
|
713
|
+
WHERE session_id = ?
|
|
714
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
715
|
+
AND priority = ?
|
|
716
|
+
AND created_at > ?
|
|
717
|
+
ORDER BY created_at DESC
|
|
718
|
+
`)
|
|
719
|
+
.all(testSessionId, '%auth%', '%auth%', 'high', oneDayAgo.toISOString());
|
|
720
|
+
expect(results).toHaveLength(1);
|
|
721
|
+
expect(results[0].key).toBe('auth_recent_high');
|
|
722
|
+
});
|
|
723
|
+
it('should combine channel and category filters', () => {
|
|
724
|
+
const results = db
|
|
725
|
+
.prepare(`
|
|
726
|
+
SELECT * FROM context_items
|
|
727
|
+
WHERE session_id = ?
|
|
728
|
+
AND channel = ?
|
|
729
|
+
AND category = ?
|
|
730
|
+
ORDER BY priority DESC, created_at DESC
|
|
731
|
+
`)
|
|
732
|
+
.all(testSessionId, 'feature/auth', 'task');
|
|
733
|
+
expect(results).toHaveLength(2);
|
|
734
|
+
expect(results.map((r) => r.key)).toContain('auth_recent_high');
|
|
735
|
+
expect(results.map((r) => r.key)).toContain('auth_old_normal');
|
|
736
|
+
});
|
|
737
|
+
it('should handle complex multi-filter queries', () => {
|
|
738
|
+
const channels = ['feature/auth', 'main'];
|
|
739
|
+
const priorities = ['high'];
|
|
740
|
+
const oneDayAgo = new Date();
|
|
741
|
+
oneDayAgo.setDate(oneDayAgo.getDate() - 1);
|
|
742
|
+
oneDayAgo.setHours(oneDayAgo.getHours() - 1);
|
|
743
|
+
const channelPlaceholders = channels.map(() => '?').join(',');
|
|
744
|
+
const priorityPlaceholders = priorities.map(() => '?').join(',');
|
|
745
|
+
const results = db
|
|
746
|
+
.prepare(`
|
|
747
|
+
SELECT * FROM context_items
|
|
748
|
+
WHERE session_id = ?
|
|
749
|
+
AND channel IN (${channelPlaceholders})
|
|
750
|
+
AND priority IN (${priorityPlaceholders})
|
|
751
|
+
AND created_at > ?
|
|
752
|
+
AND category = ?
|
|
753
|
+
ORDER BY created_at DESC
|
|
754
|
+
`)
|
|
755
|
+
.all(testSessionId, ...channels, ...priorities, oneDayAgo.toISOString(), 'task');
|
|
756
|
+
expect(results).toHaveLength(2);
|
|
757
|
+
expect(results.map((r) => r.key).sort()).toEqual(['auth_recent_high', 'db_recent_high']);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
describe('Performance with Large Dataset', () => {
|
|
761
|
+
beforeEach(() => {
|
|
762
|
+
// Create 1000+ items for performance testing
|
|
763
|
+
const channels = ['main', 'feature/auth', 'feature/ui', 'hotfix/security', 'develop'];
|
|
764
|
+
const categories = ['task', 'decision', 'note', 'error', 'warning'];
|
|
765
|
+
const priorities = ['critical', 'high', 'normal', 'low'];
|
|
766
|
+
for (let i = 0; i < 1000; i++) {
|
|
767
|
+
const channel = channels[i % channels.length];
|
|
768
|
+
const category = categories[i % categories.length];
|
|
769
|
+
const priority = priorities[i % priorities.length];
|
|
770
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority, channel) VALUES (?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(4, '0')}`, `This is the value for item ${i} with some searchable content like auth, database, api, etc.`, category, priority, channel);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
it('should search efficiently with 1000+ items', () => {
|
|
774
|
+
const start = Date.now();
|
|
775
|
+
const results = db
|
|
776
|
+
.prepare(`
|
|
777
|
+
SELECT * FROM context_items
|
|
778
|
+
WHERE session_id = ?
|
|
779
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
780
|
+
ORDER BY priority DESC, created_at DESC
|
|
781
|
+
LIMIT 50
|
|
782
|
+
`)
|
|
783
|
+
.all(testSessionId, '%auth%', '%auth%');
|
|
784
|
+
const duration = Date.now() - start;
|
|
785
|
+
expect(results).toHaveLength(50);
|
|
786
|
+
expect(duration).toBeLessThan(100); // Should complete within 100ms
|
|
787
|
+
});
|
|
788
|
+
it('should paginate efficiently through large result sets', () => {
|
|
789
|
+
const start = Date.now();
|
|
790
|
+
// Get page 3 (offset 100, limit 50)
|
|
791
|
+
const results = db
|
|
792
|
+
.prepare(`
|
|
793
|
+
SELECT * FROM context_items
|
|
794
|
+
WHERE session_id = ?
|
|
795
|
+
AND category = ?
|
|
796
|
+
ORDER BY created_at DESC
|
|
797
|
+
LIMIT 50 OFFSET 100
|
|
798
|
+
`)
|
|
799
|
+
.all(testSessionId, 'task');
|
|
800
|
+
const duration = Date.now() - start;
|
|
801
|
+
expect(results).toHaveLength(50);
|
|
802
|
+
expect(duration).toBeLessThan(50); // Pagination should be very fast
|
|
803
|
+
});
|
|
804
|
+
it('should efficiently filter by multiple criteria', () => {
|
|
805
|
+
const start = Date.now();
|
|
806
|
+
const channels = ['feature/auth', 'feature/ui'];
|
|
807
|
+
const priorities = ['high', 'critical'];
|
|
808
|
+
const channelPlaceholders = channels.map(() => '?').join(',');
|
|
809
|
+
const priorityPlaceholders = priorities.map(() => '?').join(',');
|
|
810
|
+
const _results = db
|
|
811
|
+
.prepare(`
|
|
812
|
+
SELECT * FROM context_items
|
|
813
|
+
WHERE session_id = ?
|
|
814
|
+
AND channel IN (${channelPlaceholders})
|
|
815
|
+
AND priority IN (${priorityPlaceholders})
|
|
816
|
+
AND value LIKE ?
|
|
817
|
+
ORDER BY priority DESC, created_at DESC
|
|
818
|
+
LIMIT 20
|
|
819
|
+
`)
|
|
820
|
+
.all(testSessionId, ...channels, ...priorities, '%database%');
|
|
821
|
+
const duration = Date.now() - start;
|
|
822
|
+
expect(duration).toBeLessThan(100);
|
|
823
|
+
});
|
|
824
|
+
it('should count total results efficiently', () => {
|
|
825
|
+
const start = Date.now();
|
|
826
|
+
const countResult = db
|
|
827
|
+
.prepare(`
|
|
828
|
+
SELECT COUNT(*) as count FROM context_items
|
|
829
|
+
WHERE session_id = ?
|
|
830
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
831
|
+
`)
|
|
832
|
+
.get(testSessionId, '%api%', '%api%');
|
|
833
|
+
const duration = Date.now() - start;
|
|
834
|
+
expect(countResult.count).toBeGreaterThan(0);
|
|
835
|
+
expect(duration).toBeLessThan(50); // Count should be very fast
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
describe('Edge Cases', () => {
|
|
839
|
+
it('should handle empty search results gracefully', () => {
|
|
840
|
+
const results = db
|
|
841
|
+
.prepare(`
|
|
842
|
+
SELECT * FROM context_items
|
|
843
|
+
WHERE session_id = ?
|
|
844
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
845
|
+
`)
|
|
846
|
+
.all(testSessionId, '%nonexistent%', '%nonexistent%');
|
|
847
|
+
expect(results).toHaveLength(0);
|
|
848
|
+
});
|
|
849
|
+
it('should handle invalid date formats', () => {
|
|
850
|
+
// Should not throw error with invalid date
|
|
851
|
+
const results = db
|
|
852
|
+
.prepare(`
|
|
853
|
+
SELECT * FROM context_items
|
|
854
|
+
WHERE session_id = ?
|
|
855
|
+
AND created_at > ?
|
|
856
|
+
`)
|
|
857
|
+
.all(testSessionId, 'invalid-date');
|
|
858
|
+
// SQLite will handle invalid dates gracefully
|
|
859
|
+
expect(Array.isArray(results)).toBe(true);
|
|
860
|
+
});
|
|
861
|
+
it('should handle special characters in search queries', () => {
|
|
862
|
+
// Add item with special characters
|
|
863
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'special_chars', 'Value with % and _ special chars');
|
|
864
|
+
// Search for literal % character (need to escape in LIKE)
|
|
865
|
+
const results = db
|
|
866
|
+
.prepare(`
|
|
867
|
+
SELECT * FROM context_items
|
|
868
|
+
WHERE session_id = ?
|
|
869
|
+
AND value LIKE ? ESCAPE '\\'
|
|
870
|
+
`)
|
|
871
|
+
.all(testSessionId, '%\\%%');
|
|
872
|
+
expect(results.some((r) => r.key === 'special_chars')).toBe(true);
|
|
873
|
+
});
|
|
874
|
+
it('should handle null values in optional fields', () => {
|
|
875
|
+
// Add items with null values
|
|
876
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'null_fields', 'Item with nulls', null, null, null);
|
|
877
|
+
const results = db
|
|
878
|
+
.prepare(`
|
|
879
|
+
SELECT * FROM context_items
|
|
880
|
+
WHERE session_id = ?
|
|
881
|
+
AND category IS NULL
|
|
882
|
+
`)
|
|
883
|
+
.all(testSessionId);
|
|
884
|
+
expect(results.some((r) => r.key === 'null_fields')).toBe(true);
|
|
885
|
+
});
|
|
886
|
+
it('should handle very long search queries', () => {
|
|
887
|
+
const longQuery = 'a'.repeat(1000);
|
|
888
|
+
const results = db
|
|
889
|
+
.prepare(`
|
|
890
|
+
SELECT * FROM context_items
|
|
891
|
+
WHERE session_id = ?
|
|
892
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
893
|
+
`)
|
|
894
|
+
.all(testSessionId, `%${longQuery}%`, `%${longQuery}%`);
|
|
895
|
+
expect(results).toHaveLength(0);
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
describe('SearchIn Parameter Behavior', () => {
|
|
899
|
+
beforeEach(() => {
|
|
900
|
+
// Add test data with specific patterns
|
|
901
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'auth_key_only', 'This value does not contain the search term');
|
|
902
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'normal_key', 'This value contains auth in the content');
|
|
903
|
+
});
|
|
904
|
+
it('should search in both key and value by default', () => {
|
|
905
|
+
const results = db
|
|
906
|
+
.prepare(`
|
|
907
|
+
SELECT * FROM context_items
|
|
908
|
+
WHERE session_id = ?
|
|
909
|
+
AND (key LIKE ? OR value LIKE ?)
|
|
910
|
+
`)
|
|
911
|
+
.all(testSessionId, '%auth%', '%auth%');
|
|
912
|
+
expect(results).toHaveLength(2);
|
|
913
|
+
expect(results.map((r) => r.key)).toContain('auth_key_only');
|
|
914
|
+
expect(results.map((r) => r.key)).toContain('normal_key');
|
|
915
|
+
});
|
|
916
|
+
it('should search only in keys when searchIn = ["key"]', () => {
|
|
917
|
+
const results = db
|
|
918
|
+
.prepare(`
|
|
919
|
+
SELECT * FROM context_items
|
|
920
|
+
WHERE session_id = ?
|
|
921
|
+
AND key LIKE ?
|
|
922
|
+
`)
|
|
923
|
+
.all(testSessionId, '%auth%');
|
|
924
|
+
expect(results).toHaveLength(1);
|
|
925
|
+
expect(results[0].key).toBe('auth_key_only');
|
|
926
|
+
});
|
|
927
|
+
it('should search only in values when searchIn = ["value"]', () => {
|
|
928
|
+
const results = db
|
|
929
|
+
.prepare(`
|
|
930
|
+
SELECT * FROM context_items
|
|
931
|
+
WHERE session_id = ?
|
|
932
|
+
AND value LIKE ?
|
|
933
|
+
`)
|
|
934
|
+
.all(testSessionId, '%auth%');
|
|
935
|
+
expect(results).toHaveLength(1);
|
|
936
|
+
expect(results[0].key).toBe('normal_key');
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
describe('Multi-word AND/OR Search (matchMode)', () => {
|
|
940
|
+
let contextRepo;
|
|
941
|
+
let sessionId;
|
|
942
|
+
beforeEach(() => {
|
|
943
|
+
contextRepo = new ContextRepository_1.ContextRepository(dbManager);
|
|
944
|
+
sessionId = (0, uuid_1.v4)();
|
|
945
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'matchMode test');
|
|
946
|
+
// Item: XTAR appears in key, 结汇人 appears in value
|
|
947
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'XTAR_account', '结汇人 transaction record');
|
|
948
|
+
// Item: both words in value
|
|
949
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'trade_record', 'XTAR 结汇人 settlement');
|
|
950
|
+
// Item: only XTAR
|
|
951
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'xtar_only', 'XTAR trading platform');
|
|
952
|
+
// Item: only 结汇人
|
|
953
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'jiehuiren_only', '结汇人 data point');
|
|
954
|
+
});
|
|
955
|
+
it('single-word query is backward-compatible', () => {
|
|
956
|
+
const { items } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId });
|
|
957
|
+
expect(items.length).toBe(3);
|
|
958
|
+
expect(items.map(i => i.key)).toEqual(expect.arrayContaining(['XTAR_account', 'trade_record', 'xtar_only']));
|
|
959
|
+
});
|
|
960
|
+
it('multi-word AND (default) requires all terms to match', () => {
|
|
961
|
+
const { items } = contextRepo.searchEnhanced({ query: 'XTAR 结汇人', sessionId });
|
|
962
|
+
expect(items.length).toBe(2);
|
|
963
|
+
expect(items.map(i => i.key)).toEqual(expect.arrayContaining(['XTAR_account', 'trade_record']));
|
|
964
|
+
});
|
|
965
|
+
it('multi-word AND with matchMode:"and" is explicit equivalent', () => {
|
|
966
|
+
const { items } = contextRepo.searchEnhanced({
|
|
967
|
+
query: 'XTAR 结汇人',
|
|
968
|
+
sessionId,
|
|
969
|
+
matchMode: 'and',
|
|
970
|
+
});
|
|
971
|
+
expect(items.length).toBe(2);
|
|
972
|
+
});
|
|
973
|
+
it('multi-word OR returns items matching any term', () => {
|
|
974
|
+
const { items } = contextRepo.searchEnhanced({
|
|
975
|
+
query: 'XTAR 结汇人',
|
|
976
|
+
sessionId,
|
|
977
|
+
matchMode: 'or',
|
|
978
|
+
});
|
|
979
|
+
expect(items.length).toBe(4);
|
|
980
|
+
});
|
|
981
|
+
it('AND with a term that has no match returns nothing', () => {
|
|
982
|
+
const { items } = contextRepo.searchEnhanced({ query: 'XTAR nonexistent_xyz', sessionId });
|
|
983
|
+
expect(items.length).toBe(0);
|
|
984
|
+
});
|
|
985
|
+
it('searchAcrossSessionsEnhanced also supports matchMode', () => {
|
|
986
|
+
const { items: andItems } = contextRepo.searchAcrossSessionsEnhanced({
|
|
987
|
+
query: 'XTAR 结汇人',
|
|
988
|
+
currentSessionId: sessionId,
|
|
989
|
+
matchMode: 'and',
|
|
990
|
+
});
|
|
991
|
+
expect(andItems.length).toBe(2);
|
|
992
|
+
const { items: orItems } = contextRepo.searchAcrossSessionsEnhanced({
|
|
993
|
+
query: 'XTAR 结汇人',
|
|
994
|
+
currentSessionId: sessionId,
|
|
995
|
+
matchMode: 'or',
|
|
996
|
+
});
|
|
997
|
+
expect(orItems.length).toBe(4);
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
describe('FTS5 full-text search (useFts5)', () => {
|
|
1001
|
+
let contextRepo;
|
|
1002
|
+
let sessionId;
|
|
1003
|
+
let fts5Available;
|
|
1004
|
+
beforeEach(() => {
|
|
1005
|
+
contextRepo = new ContextRepository_1.ContextRepository(dbManager);
|
|
1006
|
+
sessionId = (0, uuid_1.v4)();
|
|
1007
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'fts5 test');
|
|
1008
|
+
// Check if FTS5 table was created by DatabaseManager
|
|
1009
|
+
const ftsTable = db
|
|
1010
|
+
.prepare("SELECT COUNT(*) as c FROM sqlite_master WHERE type='table' AND name='context_items_fts'")
|
|
1011
|
+
.get();
|
|
1012
|
+
fts5Available = ftsTable.c > 0;
|
|
1013
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'XTAR_account', 'settlement record for 结汇人');
|
|
1014
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'trade_log', 'XTAR platform trade entry');
|
|
1015
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, 0)').run((0, uuid_1.v4)(), sessionId, 'other_entry', 'unrelated content');
|
|
1016
|
+
});
|
|
1017
|
+
it('useFts5:true finds results for ≥3-char terms (or falls back to LIKE)', () => {
|
|
1018
|
+
const { items } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId, useFts5: true });
|
|
1019
|
+
// Both FTS5 and LIKE fallback should find the two XTAR items
|
|
1020
|
+
expect(items.length).toBe(2);
|
|
1021
|
+
});
|
|
1022
|
+
it('useFts5:true with short (<3-char) term auto-falls-back to LIKE', () => {
|
|
1023
|
+
// "汇" is 1 CJK char — below trigram minimum; should still work via LIKE fallback
|
|
1024
|
+
const { items } = contextRepo.searchEnhanced({ query: '汇', sessionId, useFts5: true });
|
|
1025
|
+
// LIKE fallback finds '结汇人' in the value column
|
|
1026
|
+
expect(items.length).toBeGreaterThanOrEqual(1);
|
|
1027
|
+
});
|
|
1028
|
+
it('useFts5:true with multi-word query finds AND intersection', () => {
|
|
1029
|
+
const { items } = contextRepo.searchEnhanced({
|
|
1030
|
+
query: 'XTAR settlement',
|
|
1031
|
+
sessionId,
|
|
1032
|
+
useFts5: true,
|
|
1033
|
+
});
|
|
1034
|
+
if (fts5Available) {
|
|
1035
|
+
// FTS5: both terms must appear → only XTAR_account
|
|
1036
|
+
expect(items.length).toBe(1);
|
|
1037
|
+
expect(items[0].key).toBe('XTAR_account');
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
// LIKE fallback also works
|
|
1041
|
+
expect(items.length).toBeGreaterThanOrEqual(0);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
it('useFts5:false behaves identically to LIKE search', () => {
|
|
1045
|
+
const { items: ftsOff } = contextRepo.searchEnhanced({
|
|
1046
|
+
query: 'XTAR',
|
|
1047
|
+
sessionId,
|
|
1048
|
+
useFts5: false,
|
|
1049
|
+
});
|
|
1050
|
+
const { items: likeSearch } = contextRepo.searchEnhanced({ query: 'XTAR', sessionId });
|
|
1051
|
+
expect(ftsOff.length).toBe(likeSearch.length);
|
|
1052
|
+
});
|
|
1053
|
+
});
|
|
1054
|
+
});
|