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,245 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const globals_1 = require("@jest/globals");
|
|
37
|
+
const database_1 = require("../../utils/database");
|
|
38
|
+
const RepositoryManager_1 = require("../../repositories/RepositoryManager");
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
(0, globals_1.describe)('Backward Compatibility Tests', () => {
|
|
43
|
+
let dbManager;
|
|
44
|
+
let tempDbPath;
|
|
45
|
+
let repositories;
|
|
46
|
+
let testSessionId;
|
|
47
|
+
let _server;
|
|
48
|
+
// Mock handler function to test actual handler logic
|
|
49
|
+
async function callContextGet(args) {
|
|
50
|
+
// This simulates the actual handler logic from index.ts
|
|
51
|
+
const { key, category, channel, channels, sessionId: specificSessionId, includeMetadata, sort, limit, offset, createdAfter, createdBefore, keyPattern, priorities, } = args;
|
|
52
|
+
const targetSessionId = specificSessionId || testSessionId;
|
|
53
|
+
// Use enhanced query for complex queries or when we need pagination
|
|
54
|
+
if (sort !== undefined ||
|
|
55
|
+
limit !== undefined ||
|
|
56
|
+
offset ||
|
|
57
|
+
createdAfter ||
|
|
58
|
+
createdBefore ||
|
|
59
|
+
keyPattern ||
|
|
60
|
+
priorities ||
|
|
61
|
+
channel ||
|
|
62
|
+
channels ||
|
|
63
|
+
includeMetadata ||
|
|
64
|
+
(!key && !category) // If listing all items without filters, use pagination
|
|
65
|
+
) {
|
|
66
|
+
const result = repositories.contexts.queryEnhanced({
|
|
67
|
+
sessionId: targetSessionId,
|
|
68
|
+
key,
|
|
69
|
+
category,
|
|
70
|
+
channel,
|
|
71
|
+
channels,
|
|
72
|
+
sort,
|
|
73
|
+
limit,
|
|
74
|
+
offset,
|
|
75
|
+
createdAfter,
|
|
76
|
+
createdBefore,
|
|
77
|
+
keyPattern,
|
|
78
|
+
priorities,
|
|
79
|
+
includeMetadata,
|
|
80
|
+
});
|
|
81
|
+
if (result.items.length === 0) {
|
|
82
|
+
return { content: [{ type: 'text', text: 'No matching context found' }] };
|
|
83
|
+
}
|
|
84
|
+
// Return enhanced format
|
|
85
|
+
const response = {
|
|
86
|
+
items: result.items,
|
|
87
|
+
pagination: {
|
|
88
|
+
total: result.totalCount,
|
|
89
|
+
returned: result.items.length,
|
|
90
|
+
offset: offset || 0,
|
|
91
|
+
hasMore: false, // Simplified for test
|
|
92
|
+
nextOffset: null,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// Backward compatible simple queries
|
|
100
|
+
let rows;
|
|
101
|
+
if (key) {
|
|
102
|
+
const item = repositories.contexts.getAccessibleByKey(targetSessionId, key);
|
|
103
|
+
rows = item ? [item] : [];
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
rows = repositories.contexts.getAccessibleItems(targetSessionId, { category });
|
|
107
|
+
}
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
return { content: [{ type: 'text', text: 'No matching context found' }] };
|
|
110
|
+
}
|
|
111
|
+
if (key && rows.length === 1) {
|
|
112
|
+
// Single item requested - return just the value
|
|
113
|
+
const item = rows[0];
|
|
114
|
+
return { content: [{ type: 'text', text: item.value }] };
|
|
115
|
+
}
|
|
116
|
+
// Multiple items - return formatted list
|
|
117
|
+
const items = rows
|
|
118
|
+
.map((r) => `• [${r.priority}] ${r.key}: ${r.value.substring(0, 100)}${r.value.length > 100 ? '...' : ''}`)
|
|
119
|
+
.join('\n');
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: 'text', text: `Found ${rows.length} context items:\n\n${items}` }],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
(0, globals_1.beforeEach)(() => {
|
|
125
|
+
tempDbPath = path.join(os.tmpdir(), `test-backward-compat-${Date.now()}.db`);
|
|
126
|
+
dbManager = new database_1.DatabaseManager({
|
|
127
|
+
filename: tempDbPath,
|
|
128
|
+
maxSize: 10 * 1024 * 1024,
|
|
129
|
+
walMode: true,
|
|
130
|
+
});
|
|
131
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
132
|
+
// Create test session
|
|
133
|
+
const session = repositories.sessions.create({ name: 'Test Session' });
|
|
134
|
+
testSessionId = session.id;
|
|
135
|
+
// Create test data
|
|
136
|
+
repositories.contexts.save(testSessionId, {
|
|
137
|
+
key: 'single.item',
|
|
138
|
+
value: 'This is a single item value',
|
|
139
|
+
category: 'test',
|
|
140
|
+
priority: 'normal',
|
|
141
|
+
});
|
|
142
|
+
repositories.contexts.save(testSessionId, {
|
|
143
|
+
key: 'category.item1',
|
|
144
|
+
value: 'Category test item 1',
|
|
145
|
+
category: 'testcat',
|
|
146
|
+
priority: 'high',
|
|
147
|
+
});
|
|
148
|
+
repositories.contexts.save(testSessionId, {
|
|
149
|
+
key: 'category.item2',
|
|
150
|
+
value: 'Category test item 2',
|
|
151
|
+
category: 'testcat',
|
|
152
|
+
priority: 'normal',
|
|
153
|
+
});
|
|
154
|
+
// Create many items to test pagination
|
|
155
|
+
for (let i = 0; i < 150; i++) {
|
|
156
|
+
repositories.contexts.save(testSessionId, {
|
|
157
|
+
key: `bulk.item.${i.toString().padStart(3, '0')}`,
|
|
158
|
+
value: `Bulk item value ${i}`,
|
|
159
|
+
category: 'bulk',
|
|
160
|
+
priority: i % 2 === 0 ? 'high' : 'normal',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
(0, globals_1.afterEach)(() => {
|
|
165
|
+
dbManager.close();
|
|
166
|
+
try {
|
|
167
|
+
fs.unlinkSync(tempDbPath);
|
|
168
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
169
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
170
|
+
}
|
|
171
|
+
catch (_e) {
|
|
172
|
+
// Ignore
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
(0, globals_1.describe)('Single Item Retrieval (Backward Compatible)', () => {
|
|
176
|
+
(0, globals_1.it)('should return just the value for single item by key', async () => {
|
|
177
|
+
const result = await callContextGet({ key: 'single.item' });
|
|
178
|
+
(0, globals_1.expect)(result.content[0].text).toBe('This is a single item value');
|
|
179
|
+
// Should NOT include pagination or JSON structure
|
|
180
|
+
(0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
|
|
181
|
+
(0, globals_1.expect)(result.content[0].text).not.toContain('{');
|
|
182
|
+
});
|
|
183
|
+
(0, globals_1.it)('should return "No matching context found" for non-existent key', async () => {
|
|
184
|
+
const result = await callContextGet({ key: 'non.existent' });
|
|
185
|
+
(0, globals_1.expect)(result.content[0].text).toBe('No matching context found');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
(0, globals_1.describe)('Category Filtering (Backward Compatible)', () => {
|
|
189
|
+
(0, globals_1.it)('should return formatted list for category filter only', async () => {
|
|
190
|
+
const result = await callContextGet({ category: 'testcat' });
|
|
191
|
+
(0, globals_1.expect)(result.content[0].text).toContain('Found 2 context items:');
|
|
192
|
+
(0, globals_1.expect)(result.content[0].text).toContain('• [high] category.item1:');
|
|
193
|
+
(0, globals_1.expect)(result.content[0].text).toContain('• [normal] category.item2:');
|
|
194
|
+
// Should NOT include pagination
|
|
195
|
+
(0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
(0, globals_1.describe)('Enhanced Queries (New Format)', () => {
|
|
199
|
+
(0, globals_1.it)('should return paginated format when listing all items', async () => {
|
|
200
|
+
const result = await callContextGet({});
|
|
201
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
202
|
+
(0, globals_1.expect)(parsed).toHaveProperty('items');
|
|
203
|
+
(0, globals_1.expect)(parsed).toHaveProperty('pagination');
|
|
204
|
+
(0, globals_1.expect)(parsed.pagination.total).toBe(153); // All test items
|
|
205
|
+
(0, globals_1.expect)(parsed.pagination.returned).toBe(100); // Default limit
|
|
206
|
+
});
|
|
207
|
+
(0, globals_1.it)('should return paginated format when using limit', async () => {
|
|
208
|
+
const result = await callContextGet({ category: 'bulk', limit: 10 });
|
|
209
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
210
|
+
(0, globals_1.expect)(parsed).toHaveProperty('items');
|
|
211
|
+
(0, globals_1.expect)(parsed).toHaveProperty('pagination');
|
|
212
|
+
(0, globals_1.expect)(parsed.items).toHaveLength(10);
|
|
213
|
+
(0, globals_1.expect)(parsed.pagination.returned).toBe(10);
|
|
214
|
+
});
|
|
215
|
+
(0, globals_1.it)('should return paginated format when using sort', async () => {
|
|
216
|
+
const result = await callContextGet({ category: 'bulk', sort: 'key_asc' });
|
|
217
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
218
|
+
(0, globals_1.expect)(parsed).toHaveProperty('items');
|
|
219
|
+
(0, globals_1.expect)(parsed).toHaveProperty('pagination');
|
|
220
|
+
(0, globals_1.expect)(parsed.items[0].key).toBe('bulk.item.000');
|
|
221
|
+
});
|
|
222
|
+
(0, globals_1.it)('should return paginated format when using includeMetadata', async () => {
|
|
223
|
+
const result = await callContextGet({ key: 'single.item', includeMetadata: true });
|
|
224
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
225
|
+
(0, globals_1.expect)(parsed).toHaveProperty('items');
|
|
226
|
+
(0, globals_1.expect)(parsed).toHaveProperty('pagination');
|
|
227
|
+
(0, globals_1.expect)(parsed.items).toHaveLength(1);
|
|
228
|
+
(0, globals_1.expect)(parsed.items[0]).toHaveProperty('size');
|
|
229
|
+
(0, globals_1.expect)(parsed.items[0]).toHaveProperty('created_at');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
(0, globals_1.describe)('Mixed Scenarios', () => {
|
|
233
|
+
(0, globals_1.it)('should use simple format for key+category filter', async () => {
|
|
234
|
+
// Even with both key and category, if key is specified, use simple format
|
|
235
|
+
const result = await callContextGet({ key: 'single.item', category: 'test' });
|
|
236
|
+
(0, globals_1.expect)(result.content[0].text).toBe('This is a single item value');
|
|
237
|
+
(0, globals_1.expect)(result.content[0].text).not.toContain('pagination');
|
|
238
|
+
});
|
|
239
|
+
(0, globals_1.it)('should use enhanced format when channel is specified', async () => {
|
|
240
|
+
const result = await callContextGet({ category: 'test', channel: 'general' });
|
|
241
|
+
const parsed = JSON.parse(result.content[0].text);
|
|
242
|
+
(0, globals_1.expect)(parsed).toHaveProperty('pagination');
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const globals_1 = require("@jest/globals");
|
|
37
|
+
// Removed unused imports: Server, CallToolRequestSchema
|
|
38
|
+
const database_1 = require("../../utils/database");
|
|
39
|
+
const RepositoryManager_1 = require("../../repositories/RepositoryManager");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
(0, globals_1.describe)('Batch Operations E2E Tests', () => {
|
|
44
|
+
let tempDbPath;
|
|
45
|
+
let dbManager;
|
|
46
|
+
let repositories;
|
|
47
|
+
let testSessionId;
|
|
48
|
+
// Mock the server handler
|
|
49
|
+
const mockHandleToolCall = async (toolName, args) => {
|
|
50
|
+
// This simulates what happens in index.ts
|
|
51
|
+
switch (toolName) {
|
|
52
|
+
case 'context_batch_save': {
|
|
53
|
+
const { items, updateExisting = true } = args;
|
|
54
|
+
const sessionId = testSessionId;
|
|
55
|
+
// Validate items
|
|
56
|
+
if (!items || !Array.isArray(items) || items.length === 0) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: 'No items provided for batch save',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// Validate each item
|
|
67
|
+
const validationErrors = [];
|
|
68
|
+
items.forEach((item, index) => {
|
|
69
|
+
try {
|
|
70
|
+
if (!item.key || !item.key.trim()) {
|
|
71
|
+
throw new Error('Key is required and cannot be empty');
|
|
72
|
+
}
|
|
73
|
+
if (!item.value) {
|
|
74
|
+
throw new Error('Value is required');
|
|
75
|
+
}
|
|
76
|
+
if (item.category) {
|
|
77
|
+
const validCategories = ['task', 'decision', 'progress', 'note', 'error', 'warning'];
|
|
78
|
+
if (!validCategories.includes(item.category)) {
|
|
79
|
+
throw new Error(`Invalid category: ${item.category}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (item.priority) {
|
|
83
|
+
const validPriorities = ['high', 'normal', 'low'];
|
|
84
|
+
if (!validPriorities.includes(item.priority)) {
|
|
85
|
+
throw new Error(`Invalid priority: ${item.priority}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
validationErrors.push({
|
|
91
|
+
index,
|
|
92
|
+
key: item.key || 'undefined',
|
|
93
|
+
error: error.message,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
// Filter out items with validation errors before passing to repository
|
|
98
|
+
const validItemIndices = [];
|
|
99
|
+
const validItems = items.filter((_, index) => {
|
|
100
|
+
const hasError = validationErrors.some(err => err.index === index);
|
|
101
|
+
if (!hasError) {
|
|
102
|
+
validItemIndices.push(index);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
});
|
|
107
|
+
if (validationErrors.length === items.length) {
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: 'text',
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
operation: 'batch_save',
|
|
114
|
+
totalItems: items.length,
|
|
115
|
+
succeeded: 0,
|
|
116
|
+
failed: validationErrors.length,
|
|
117
|
+
totalSize: 0,
|
|
118
|
+
results: [],
|
|
119
|
+
errors: validationErrors,
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
}, null, 2),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let results = [];
|
|
127
|
+
let totalSize = 0;
|
|
128
|
+
dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
|
|
129
|
+
try {
|
|
130
|
+
// Track original indices
|
|
131
|
+
const indexMap = new Map();
|
|
132
|
+
validItems.forEach((item, newIdx) => {
|
|
133
|
+
indexMap.set(newIdx, items.indexOf(item));
|
|
134
|
+
});
|
|
135
|
+
const batchResult = repositories.contexts.batchSave(sessionId, validItems, {
|
|
136
|
+
updateExisting,
|
|
137
|
+
});
|
|
138
|
+
totalSize = batchResult.totalSize;
|
|
139
|
+
// Map results back to original indices
|
|
140
|
+
results = batchResult.results
|
|
141
|
+
.filter(r => r.success)
|
|
142
|
+
.map(r => ({
|
|
143
|
+
...r,
|
|
144
|
+
index: indexMap.get(r.index) ?? r.index,
|
|
145
|
+
}));
|
|
146
|
+
const errors = [
|
|
147
|
+
...validationErrors,
|
|
148
|
+
...batchResult.results
|
|
149
|
+
.filter(r => !r.success)
|
|
150
|
+
.map(r => ({
|
|
151
|
+
index: indexMap.get(r.index) ?? r.index,
|
|
152
|
+
key: r.key,
|
|
153
|
+
error: r.error,
|
|
154
|
+
})),
|
|
155
|
+
];
|
|
156
|
+
dbManager.getDatabase().prepare('COMMIT').run();
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
operation: 'batch_save',
|
|
163
|
+
totalItems: items.length,
|
|
164
|
+
succeeded: results.length,
|
|
165
|
+
failed: errors.length,
|
|
166
|
+
totalSize: totalSize,
|
|
167
|
+
averageSize: results.length > 0 ? Math.round(totalSize / results.length) : 0,
|
|
168
|
+
results: results,
|
|
169
|
+
errors: errors,
|
|
170
|
+
timestamp: new Date().toISOString(),
|
|
171
|
+
}, null, 2),
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
dbManager.getDatabase().prepare('ROLLBACK').run();
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: `Batch save failed: ${error.message}`,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
case 'context_batch_delete': {
|
|
189
|
+
const { keys, keyPattern, dryRun = false } = args;
|
|
190
|
+
const sessionId = testSessionId;
|
|
191
|
+
if (dryRun) {
|
|
192
|
+
const itemsToDelete = repositories.contexts.getDryRunItems(sessionId, {
|
|
193
|
+
keys,
|
|
194
|
+
keyPattern,
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: JSON.stringify({
|
|
201
|
+
operation: 'batch_delete',
|
|
202
|
+
dryRun: true,
|
|
203
|
+
keys: keys,
|
|
204
|
+
pattern: keyPattern,
|
|
205
|
+
itemsToDelete: itemsToDelete,
|
|
206
|
+
totalItems: itemsToDelete.length,
|
|
207
|
+
}, null, 2),
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
|
|
213
|
+
try {
|
|
214
|
+
const deleteResult = repositories.contexts.batchDelete(sessionId, { keys, keyPattern });
|
|
215
|
+
dbManager.getDatabase().prepare('COMMIT').run();
|
|
216
|
+
const response = keys
|
|
217
|
+
? {
|
|
218
|
+
operation: 'batch_delete',
|
|
219
|
+
keys: keys,
|
|
220
|
+
totalRequested: keys.length,
|
|
221
|
+
totalDeleted: deleteResult.totalDeleted,
|
|
222
|
+
notFound: deleteResult.results?.filter(r => !r.deleted).map(r => r.key) || [],
|
|
223
|
+
results: deleteResult.results,
|
|
224
|
+
}
|
|
225
|
+
: {
|
|
226
|
+
operation: 'batch_delete',
|
|
227
|
+
pattern: keyPattern,
|
|
228
|
+
totalDeleted: deleteResult.totalDeleted,
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: 'text',
|
|
234
|
+
text: JSON.stringify(response, null, 2),
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
dbManager.getDatabase().prepare('ROLLBACK').run();
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: 'text',
|
|
245
|
+
text: `Batch delete failed: ${error.message}`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
case 'context_batch_update': {
|
|
252
|
+
const { updates } = args;
|
|
253
|
+
const sessionId = testSessionId;
|
|
254
|
+
dbManager.getDatabase().prepare('BEGIN TRANSACTION').run();
|
|
255
|
+
try {
|
|
256
|
+
const updateResult = repositories.contexts.batchUpdate(sessionId, updates);
|
|
257
|
+
dbManager.getDatabase().prepare('COMMIT').run();
|
|
258
|
+
const results = updateResult.results.filter(r => r.updated);
|
|
259
|
+
const errors = updateResult.results.filter(r => !r.updated);
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{
|
|
263
|
+
type: 'text',
|
|
264
|
+
text: JSON.stringify({
|
|
265
|
+
operation: 'batch_update',
|
|
266
|
+
totalItems: updates.length,
|
|
267
|
+
succeeded: results.length,
|
|
268
|
+
failed: errors.length,
|
|
269
|
+
results: results,
|
|
270
|
+
errors: errors,
|
|
271
|
+
}, null, 2),
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
dbManager.getDatabase().prepare('ROLLBACK').run();
|
|
278
|
+
return {
|
|
279
|
+
content: [
|
|
280
|
+
{
|
|
281
|
+
type: 'text',
|
|
282
|
+
text: `Batch update failed: ${error.message}`,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
default:
|
|
289
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
(0, globals_1.beforeEach)(() => {
|
|
293
|
+
tempDbPath = path.join(os.tmpdir(), `test-batch-e2e-${Date.now()}.db`);
|
|
294
|
+
dbManager = new database_1.DatabaseManager({ filename: tempDbPath });
|
|
295
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
296
|
+
// Create test session
|
|
297
|
+
const session = repositories.sessions.create({
|
|
298
|
+
name: 'E2E Test Session',
|
|
299
|
+
description: 'Testing batch operations E2E',
|
|
300
|
+
});
|
|
301
|
+
testSessionId = session.id;
|
|
302
|
+
});
|
|
303
|
+
(0, globals_1.afterEach)(() => {
|
|
304
|
+
dbManager.close();
|
|
305
|
+
try {
|
|
306
|
+
fs.unlinkSync(tempDbPath);
|
|
307
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
308
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
309
|
+
}
|
|
310
|
+
catch (_e) {
|
|
311
|
+
// Ignore
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
(0, globals_1.describe)('End-to-End Batch Operations', () => {
|
|
315
|
+
(0, globals_1.it)('should handle complete batch workflow', async () => {
|
|
316
|
+
// 1. Batch Save
|
|
317
|
+
const saveResponse = await mockHandleToolCall('context_batch_save', {
|
|
318
|
+
items: [
|
|
319
|
+
{ key: 'user.name', value: 'John Doe', category: 'note', priority: 'normal' },
|
|
320
|
+
{ key: 'user.email', value: 'john@example.com', category: 'note', priority: 'high' },
|
|
321
|
+
{ key: 'app.version', value: '1.0.0', category: 'note', priority: 'low' },
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
const saveResult = JSON.parse(saveResponse.content[0].text);
|
|
325
|
+
(0, globals_1.expect)(saveResult.operation).toBe('batch_save');
|
|
326
|
+
(0, globals_1.expect)(saveResult.succeeded).toBe(3);
|
|
327
|
+
(0, globals_1.expect)(saveResult.failed).toBe(0);
|
|
328
|
+
(0, globals_1.expect)(saveResult.totalSize).toBeGreaterThan(0);
|
|
329
|
+
// 2. Batch Update
|
|
330
|
+
const updateResponse = await mockHandleToolCall('context_batch_update', {
|
|
331
|
+
updates: [
|
|
332
|
+
{ key: 'user.name', value: 'Jane Doe' },
|
|
333
|
+
{ key: 'app.version', value: '2.0.0', priority: 'high' },
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
const updateResult = JSON.parse(updateResponse.content[0].text);
|
|
337
|
+
(0, globals_1.expect)(updateResult.operation).toBe('batch_update');
|
|
338
|
+
(0, globals_1.expect)(updateResult.succeeded).toBe(2);
|
|
339
|
+
(0, globals_1.expect)(updateResult.failed).toBe(0);
|
|
340
|
+
// 3. Batch Delete (Dry Run)
|
|
341
|
+
const dryRunResponse = await mockHandleToolCall('context_batch_delete', {
|
|
342
|
+
keys: ['user.email', 'app.version'],
|
|
343
|
+
dryRun: true,
|
|
344
|
+
});
|
|
345
|
+
const dryRunResult = JSON.parse(dryRunResponse.content[0].text);
|
|
346
|
+
(0, globals_1.expect)(dryRunResult.dryRun).toBe(true);
|
|
347
|
+
(0, globals_1.expect)(dryRunResult.totalItems).toBe(2);
|
|
348
|
+
// 4. Batch Delete (Actual)
|
|
349
|
+
const deleteResponse = await mockHandleToolCall('context_batch_delete', {
|
|
350
|
+
keys: ['user.email', 'app.version'],
|
|
351
|
+
});
|
|
352
|
+
const deleteResult = JSON.parse(deleteResponse.content[0].text);
|
|
353
|
+
(0, globals_1.expect)(deleteResult.operation).toBe('batch_delete');
|
|
354
|
+
(0, globals_1.expect)(deleteResult.totalDeleted).toBe(2);
|
|
355
|
+
// Verify final state
|
|
356
|
+
const remaining = repositories.contexts.getBySessionId(testSessionId);
|
|
357
|
+
(0, globals_1.expect)(remaining.length).toBe(1);
|
|
358
|
+
(0, globals_1.expect)(remaining[0].key).toBe('user.name');
|
|
359
|
+
(0, globals_1.expect)(remaining[0].value).toBe('Jane Doe');
|
|
360
|
+
});
|
|
361
|
+
(0, globals_1.it)('should handle validation errors gracefully', async () => {
|
|
362
|
+
const response = await mockHandleToolCall('context_batch_save', {
|
|
363
|
+
items: [
|
|
364
|
+
{ key: '', value: 'No key' },
|
|
365
|
+
{ key: 'no.value' },
|
|
366
|
+
{ key: 'valid.item', value: 'Valid' },
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
const result = JSON.parse(response.content[0].text);
|
|
370
|
+
(0, globals_1.expect)(result.succeeded).toBe(1);
|
|
371
|
+
(0, globals_1.expect)(result.failed).toBe(2);
|
|
372
|
+
(0, globals_1.expect)(result.errors).toHaveLength(2);
|
|
373
|
+
});
|
|
374
|
+
(0, globals_1.it)('should handle pattern-based operations', async () => {
|
|
375
|
+
// Setup test data
|
|
376
|
+
await mockHandleToolCall('context_batch_save', {
|
|
377
|
+
items: [
|
|
378
|
+
{ key: 'temp.file1', value: 'File 1' },
|
|
379
|
+
{ key: 'temp.file2', value: 'File 2' },
|
|
380
|
+
{ key: 'temp.cache', value: 'Cache' },
|
|
381
|
+
{ key: 'keep.this', value: 'Keep' },
|
|
382
|
+
],
|
|
383
|
+
});
|
|
384
|
+
// Delete by pattern
|
|
385
|
+
const response = await mockHandleToolCall('context_batch_delete', {
|
|
386
|
+
keyPattern: 'temp.*',
|
|
387
|
+
});
|
|
388
|
+
const result = JSON.parse(response.content[0].text);
|
|
389
|
+
(0, globals_1.expect)(result.totalDeleted).toBe(3);
|
|
390
|
+
// Verify only non-matching item remains
|
|
391
|
+
const remaining = repositories.contexts.getBySessionId(testSessionId);
|
|
392
|
+
(0, globals_1.expect)(remaining.length).toBe(1);
|
|
393
|
+
(0, globals_1.expect)(remaining[0].key).toBe('keep.this');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
});
|