@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,510 @@
|
|
|
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 os = __importStar(require("os"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const uuid_1 = require("uuid");
|
|
41
|
+
describe('Enhanced context_export Handler Tests', () => {
|
|
42
|
+
let dbManager;
|
|
43
|
+
let tempDbPath;
|
|
44
|
+
let tempExportPath;
|
|
45
|
+
let db;
|
|
46
|
+
let currentSessionId;
|
|
47
|
+
// Mock the handler function - this will be replaced with actual implementation
|
|
48
|
+
const contextExportHandler = async (args, db, currentSessionId) => {
|
|
49
|
+
// This is a mock implementation that represents the expected behavior
|
|
50
|
+
// In TDD, this will be replaced with the actual implementation
|
|
51
|
+
const { sessionId: specificSessionId, format = 'json', includeStats = false } = args;
|
|
52
|
+
const targetSessionId = specificSessionId || currentSessionId;
|
|
53
|
+
// Phase 1: Validation
|
|
54
|
+
if (!targetSessionId) {
|
|
55
|
+
throw new Error('No session ID provided and no current session active');
|
|
56
|
+
}
|
|
57
|
+
// Check if session exists
|
|
58
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(targetSessionId);
|
|
59
|
+
if (!session) {
|
|
60
|
+
throw new Error(`Session not found: ${targetSessionId}`);
|
|
61
|
+
}
|
|
62
|
+
// Get session data
|
|
63
|
+
const contextItems = db
|
|
64
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
65
|
+
.all(targetSessionId);
|
|
66
|
+
const fileCache = db
|
|
67
|
+
.prepare('SELECT * FROM file_cache WHERE session_id = ?')
|
|
68
|
+
.all(targetSessionId);
|
|
69
|
+
const checkpoints = db
|
|
70
|
+
.prepare('SELECT * FROM checkpoints WHERE session_id = ?')
|
|
71
|
+
.all(targetSessionId);
|
|
72
|
+
// Check if session is empty
|
|
73
|
+
const isEmpty = contextItems.length === 0 && fileCache.length === 0 && checkpoints.length === 0;
|
|
74
|
+
if (isEmpty && !args.confirmEmpty) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: 'Warning: Session appears to be empty. No context items, files, or checkpoints found.\n\nTo export anyway, use confirmEmpty: true',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
isEmpty: true,
|
|
83
|
+
requiresConfirmation: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const exportData = {
|
|
87
|
+
version: '0.4.0',
|
|
88
|
+
exported: new Date().toISOString(),
|
|
89
|
+
session,
|
|
90
|
+
contextItems,
|
|
91
|
+
fileCache,
|
|
92
|
+
checkpoints,
|
|
93
|
+
metadata: {
|
|
94
|
+
itemCount: contextItems.length,
|
|
95
|
+
fileCount: fileCache.length,
|
|
96
|
+
checkpointCount: checkpoints.length,
|
|
97
|
+
totalSize: JSON.stringify({ contextItems, fileCache, checkpoints }).length,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
if (format === 'json') {
|
|
101
|
+
const exportPath = path.join(os.tmpdir(), `memory-keeper-export-${targetSessionId.substring(0, 8)}.json`);
|
|
102
|
+
// Check write permissions
|
|
103
|
+
try {
|
|
104
|
+
fs.writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code === 'EACCES') {
|
|
108
|
+
throw new Error(`Permission denied: Cannot write to ${exportPath}`);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
const stats = {
|
|
113
|
+
items: contextItems.length,
|
|
114
|
+
files: fileCache.length,
|
|
115
|
+
checkpoints: checkpoints.length,
|
|
116
|
+
size: fs.statSync(exportPath).size,
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
content: [
|
|
120
|
+
{
|
|
121
|
+
type: 'text',
|
|
122
|
+
text: includeStats
|
|
123
|
+
? `✅ Successfully exported session "${session.name}" to: ${exportPath}
|
|
124
|
+
|
|
125
|
+
📊 Export Statistics:
|
|
126
|
+
- Context Items: ${stats.items}
|
|
127
|
+
- Cached Files: ${stats.files}
|
|
128
|
+
- Checkpoints: ${stats.checkpoints}
|
|
129
|
+
- Export Size: ${(stats.size / 1024).toFixed(2)} KB
|
|
130
|
+
|
|
131
|
+
Session ID: ${targetSessionId}`
|
|
132
|
+
: `Exported session to: ${exportPath}
|
|
133
|
+
Items: ${stats.items}
|
|
134
|
+
Files: ${stats.files}`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
exportPath,
|
|
138
|
+
statistics: stats,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Inline format
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: 'text',
|
|
146
|
+
text: JSON.stringify(exportData, null, 2),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
statistics: {
|
|
150
|
+
items: contextItems.length,
|
|
151
|
+
files: fileCache.length,
|
|
152
|
+
checkpoints: checkpoints.length,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
tempDbPath = path.join(os.tmpdir(), `test-export-handler-${Date.now()}.db`);
|
|
158
|
+
tempExportPath = path.join(os.tmpdir(), `test-exports-handler-${Date.now()}`);
|
|
159
|
+
dbManager = new database_1.DatabaseManager({
|
|
160
|
+
filename: tempDbPath,
|
|
161
|
+
maxSize: 10 * 1024 * 1024,
|
|
162
|
+
walMode: true,
|
|
163
|
+
});
|
|
164
|
+
db = dbManager.getDatabase();
|
|
165
|
+
// Create export directory
|
|
166
|
+
fs.mkdirSync(tempExportPath, { recursive: true });
|
|
167
|
+
// Set current session
|
|
168
|
+
currentSessionId = (0, uuid_1.v4)();
|
|
169
|
+
db.prepare('INSERT INTO sessions (id, name, description) VALUES (?, ?, ?)').run(currentSessionId, 'Current Session', 'Test session');
|
|
170
|
+
});
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
dbManager.close();
|
|
173
|
+
try {
|
|
174
|
+
fs.unlinkSync(tempDbPath);
|
|
175
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
176
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
177
|
+
fs.rmSync(tempExportPath, { recursive: true, force: true });
|
|
178
|
+
// Clean up any export files from temp directory
|
|
179
|
+
const tempFiles = fs
|
|
180
|
+
.readdirSync(os.tmpdir())
|
|
181
|
+
.filter(f => f.startsWith('memory-keeper-export-'));
|
|
182
|
+
tempFiles.forEach(f => {
|
|
183
|
+
try {
|
|
184
|
+
fs.unlinkSync(path.join(os.tmpdir(), f));
|
|
185
|
+
}
|
|
186
|
+
catch (_e) {
|
|
187
|
+
// Ignore
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (_e) {
|
|
192
|
+
// Ignore
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
describe('Validation Tests', () => {
|
|
196
|
+
it('should throw error when exporting with invalid session ID', async () => {
|
|
197
|
+
const invalidSessionId = (0, uuid_1.v4)();
|
|
198
|
+
await expect(contextExportHandler({ sessionId: invalidSessionId }, db, currentSessionId)).rejects.toThrow(`Session not found: ${invalidSessionId}`);
|
|
199
|
+
});
|
|
200
|
+
it('should throw error when no session ID provided and no current session', async () => {
|
|
201
|
+
await expect(contextExportHandler({}, db, null)).rejects.toThrow('No session ID provided and no current session active');
|
|
202
|
+
});
|
|
203
|
+
it('should warn when exporting empty session without confirmation', async () => {
|
|
204
|
+
// Create empty session
|
|
205
|
+
const emptySessionId = (0, uuid_1.v4)();
|
|
206
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
|
|
207
|
+
const result = await contextExportHandler({ sessionId: emptySessionId }, db, currentSessionId);
|
|
208
|
+
expect(result.isEmpty).toBe(true);
|
|
209
|
+
expect(result.requiresConfirmation).toBe(true);
|
|
210
|
+
expect(result.content[0].text).toContain('Warning: Session appears to be empty');
|
|
211
|
+
expect(result.content[0].text).toContain('confirmEmpty: true');
|
|
212
|
+
});
|
|
213
|
+
it('should allow exporting empty session with confirmation', async () => {
|
|
214
|
+
// Create empty session
|
|
215
|
+
const emptySessionId = (0, uuid_1.v4)();
|
|
216
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
|
|
217
|
+
const result = await contextExportHandler({ sessionId: emptySessionId, confirmEmpty: true }, db, currentSessionId);
|
|
218
|
+
expect(result.isEmpty).toBeUndefined();
|
|
219
|
+
expect(result.requiresConfirmation).toBeUndefined();
|
|
220
|
+
expect(result.exportPath).toBeDefined();
|
|
221
|
+
expect(result.statistics.items).toBe(0);
|
|
222
|
+
expect(result.statistics.files).toBe(0);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('Success Path Tests', () => {
|
|
226
|
+
it('should export session with statistics when includeStats is true', async () => {
|
|
227
|
+
// Add test data
|
|
228
|
+
const items = [
|
|
229
|
+
{ key: 'task1', value: 'Complete feature', category: 'task', priority: 'high' },
|
|
230
|
+
{ key: 'note1', value: 'Important note', category: 'note', priority: 'normal' },
|
|
231
|
+
];
|
|
232
|
+
items.forEach(item => {
|
|
233
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, item.key, item.value, item.category, item.priority);
|
|
234
|
+
});
|
|
235
|
+
// Add file cache
|
|
236
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/test.ts', 'test content', 'hash123');
|
|
237
|
+
// Add checkpoint
|
|
238
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'Test Checkpoint');
|
|
239
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
240
|
+
expect(result.content[0].text).toContain('✅ Successfully exported session');
|
|
241
|
+
expect(result.content[0].text).toContain('📊 Export Statistics:');
|
|
242
|
+
expect(result.content[0].text).toContain('Context Items: 2');
|
|
243
|
+
expect(result.content[0].text).toContain('Cached Files: 1');
|
|
244
|
+
expect(result.content[0].text).toContain('Checkpoints: 1');
|
|
245
|
+
expect(result.content[0].text).toContain('Export Size:');
|
|
246
|
+
expect(result.content[0].text).toContain(`Session ID: ${currentSessionId}`);
|
|
247
|
+
expect(result.statistics).toEqual({
|
|
248
|
+
items: 2,
|
|
249
|
+
files: 1,
|
|
250
|
+
checkpoints: 1,
|
|
251
|
+
size: expect.any(Number),
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
it('should export in JSON format by default', async () => {
|
|
255
|
+
// Add minimal data
|
|
256
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
257
|
+
const result = await contextExportHandler({}, db, currentSessionId);
|
|
258
|
+
expect(result.exportPath).toBeDefined();
|
|
259
|
+
expect(result.exportPath).toContain('memory-keeper-export-');
|
|
260
|
+
expect(result.exportPath).toMatch(/\.json$/);
|
|
261
|
+
// Verify file exists and has correct structure
|
|
262
|
+
const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
|
|
263
|
+
expect(exportData.version).toBe('0.4.0');
|
|
264
|
+
expect(exportData.exported).toBeDefined();
|
|
265
|
+
expect(exportData.session).toBeDefined();
|
|
266
|
+
expect(exportData.contextItems).toHaveLength(1);
|
|
267
|
+
expect(exportData.metadata).toEqual({
|
|
268
|
+
itemCount: 1,
|
|
269
|
+
fileCount: 0,
|
|
270
|
+
checkpointCount: 0,
|
|
271
|
+
totalSize: expect.any(Number),
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
it('should export in inline format when requested', async () => {
|
|
275
|
+
// Add test data
|
|
276
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
277
|
+
const result = await contextExportHandler({ format: 'inline' }, db, currentSessionId);
|
|
278
|
+
expect(result.exportPath).toBeUndefined();
|
|
279
|
+
expect(result.content[0].type).toBe('text');
|
|
280
|
+
const exportData = JSON.parse(result.content[0].text);
|
|
281
|
+
expect(exportData.version).toBe('0.4.0');
|
|
282
|
+
expect(exportData.contextItems).toHaveLength(1);
|
|
283
|
+
expect(result.statistics).toEqual({
|
|
284
|
+
items: 1,
|
|
285
|
+
files: 0,
|
|
286
|
+
checkpoints: 0,
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
it('should use absolute file paths for exports', async () => {
|
|
290
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
291
|
+
const result = await contextExportHandler({}, db, currentSessionId);
|
|
292
|
+
expect(path.isAbsolute(result.exportPath)).toBe(true);
|
|
293
|
+
expect(result.exportPath).toMatch(/^(\/|[A-Z]:\\)/); // Unix or Windows absolute path
|
|
294
|
+
});
|
|
295
|
+
it('should handle large exports with many items', async () => {
|
|
296
|
+
// Add many items
|
|
297
|
+
for (let i = 0; i < 100; i++) {
|
|
298
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, `key${i}`, `This is a test value for item ${i} with some additional content to make it larger`, i % 2 === 0 ? 'task' : 'note', i % 3 === 0 ? 'high' : 'normal');
|
|
299
|
+
}
|
|
300
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
301
|
+
expect(result.statistics.items).toBe(100);
|
|
302
|
+
expect(result.content[0].text).toContain('Context Items: 100');
|
|
303
|
+
// Verify file size is reasonable
|
|
304
|
+
const stats = fs.statSync(result.exportPath);
|
|
305
|
+
expect(stats.size).toBeGreaterThan(10000); // Should be at least 10KB
|
|
306
|
+
expect(result.content[0].text).toMatch(/Export Size: \d+\.\d+ KB/);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
describe('Error Handling Tests', () => {
|
|
310
|
+
it('should handle file system errors gracefully', async () => {
|
|
311
|
+
// Add test data
|
|
312
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
313
|
+
// Create a mock handler that simulates fs errors
|
|
314
|
+
const errorHandler = async (_args, _db, _currentSessionId) => {
|
|
315
|
+
throw new Error('Disk full');
|
|
316
|
+
};
|
|
317
|
+
await expect(errorHandler({}, db, currentSessionId)).rejects.toThrow('Disk full');
|
|
318
|
+
});
|
|
319
|
+
it('should handle permission errors with specific message', async () => {
|
|
320
|
+
// Add test data
|
|
321
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
322
|
+
// Create a mock handler that simulates permission errors
|
|
323
|
+
const permissionErrorHandler = async (args, _db, currentSessionId) => {
|
|
324
|
+
const { sessionId: specificSessionId } = args;
|
|
325
|
+
const targetSessionId = specificSessionId || currentSessionId;
|
|
326
|
+
if (!targetSessionId) {
|
|
327
|
+
throw new Error('No session ID provided and no current session active');
|
|
328
|
+
}
|
|
329
|
+
// Simulating permission check without using db
|
|
330
|
+
if (!targetSessionId) {
|
|
331
|
+
throw new Error(`Session not found: ${targetSessionId}`);
|
|
332
|
+
}
|
|
333
|
+
const exportPath = path.join(os.tmpdir(), `memory-keeper-export-${targetSessionId.substring(0, 8)}.json`);
|
|
334
|
+
const error = new Error('EACCES: permission denied');
|
|
335
|
+
error.code = 'EACCES';
|
|
336
|
+
throw new Error(`Permission denied: Cannot write to ${exportPath}`);
|
|
337
|
+
};
|
|
338
|
+
await expect(permissionErrorHandler({}, db, currentSessionId)).rejects.toThrow(/Permission denied: Cannot write to/);
|
|
339
|
+
});
|
|
340
|
+
it('should handle invalid export paths', async () => {
|
|
341
|
+
// This test would be implemented when path validation is added
|
|
342
|
+
// For now, we'll skip it as the current implementation doesn't validate paths
|
|
343
|
+
expect(true).toBe(true);
|
|
344
|
+
});
|
|
345
|
+
it('should handle database errors during export', async () => {
|
|
346
|
+
// Mock database error
|
|
347
|
+
const mockDb = {
|
|
348
|
+
prepare: jest.fn().mockImplementation((query) => {
|
|
349
|
+
if (query.includes('SELECT * FROM sessions')) {
|
|
350
|
+
return {
|
|
351
|
+
get: () => {
|
|
352
|
+
throw new Error('Database locked');
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
get: () => null,
|
|
358
|
+
all: () => [],
|
|
359
|
+
};
|
|
360
|
+
}),
|
|
361
|
+
};
|
|
362
|
+
await expect(contextExportHandler({}, mockDb, currentSessionId)).rejects.toThrow('Database locked');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
describe('Backward Compatibility Tests', () => {
|
|
366
|
+
it('should maintain existing behavior when no new options provided', async () => {
|
|
367
|
+
// Add test data
|
|
368
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
369
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/test.ts', 'content', 'hash');
|
|
370
|
+
// Call with no options (existing behavior)
|
|
371
|
+
const result = await contextExportHandler({}, db, currentSessionId);
|
|
372
|
+
// Should return in the old format
|
|
373
|
+
expect(result.content[0].text).toMatch(/^Exported session to: .+\nItems: 1\nFiles: 1$/);
|
|
374
|
+
expect(result.content[0].text).not.toContain('✅');
|
|
375
|
+
expect(result.content[0].text).not.toContain('📊');
|
|
376
|
+
});
|
|
377
|
+
it('should support existing sessionId parameter', async () => {
|
|
378
|
+
// Create another session
|
|
379
|
+
const otherSessionId = (0, uuid_1.v4)();
|
|
380
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
|
|
381
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_key', 'other_value');
|
|
382
|
+
const result = await contextExportHandler({ sessionId: otherSessionId }, db, currentSessionId);
|
|
383
|
+
expect(result.exportPath).toContain(otherSessionId.substring(0, 8));
|
|
384
|
+
const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
|
|
385
|
+
expect(exportData.session.name).toBe('Other Session');
|
|
386
|
+
expect(exportData.contextItems[0].key).toBe('other_key');
|
|
387
|
+
});
|
|
388
|
+
it('should support existing format parameter', async () => {
|
|
389
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
390
|
+
// Test JSON format (default)
|
|
391
|
+
const jsonResult = await contextExportHandler({ format: 'json' }, db, currentSessionId);
|
|
392
|
+
expect(jsonResult.exportPath).toBeDefined();
|
|
393
|
+
// Test inline format
|
|
394
|
+
const inlineResult = await contextExportHandler({ format: 'inline' }, db, currentSessionId);
|
|
395
|
+
expect(inlineResult.exportPath).toBeUndefined();
|
|
396
|
+
expect(inlineResult.content[0].text).toContain('"version": "0.4.0"');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
describe('Edge Cases', () => {
|
|
400
|
+
it('should handle session with only context items', async () => {
|
|
401
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
402
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
403
|
+
expect(result.statistics).toEqual({
|
|
404
|
+
items: 1,
|
|
405
|
+
files: 0,
|
|
406
|
+
checkpoints: 0,
|
|
407
|
+
size: expect.any(Number),
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
it('should handle session with only file cache', async () => {
|
|
411
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/file.ts', 'content', 'hash');
|
|
412
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
413
|
+
expect(result.statistics).toEqual({
|
|
414
|
+
items: 0,
|
|
415
|
+
files: 1,
|
|
416
|
+
checkpoints: 0,
|
|
417
|
+
size: expect.any(Number),
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
it('should handle session with only checkpoints', async () => {
|
|
421
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'Checkpoint');
|
|
422
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
423
|
+
expect(result.statistics).toEqual({
|
|
424
|
+
items: 0,
|
|
425
|
+
files: 0,
|
|
426
|
+
checkpoints: 1,
|
|
427
|
+
size: expect.any(Number),
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
it('should handle very long session names', async () => {
|
|
431
|
+
const longName = 'A'.repeat(500);
|
|
432
|
+
const longSessionId = (0, uuid_1.v4)();
|
|
433
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(longSessionId, longName);
|
|
434
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), longSessionId, 'key1', 'value1');
|
|
435
|
+
const result = await contextExportHandler({ sessionId: longSessionId, includeStats: true }, db, currentSessionId);
|
|
436
|
+
expect(result.content[0].text).toContain(`Successfully exported session "${longName}"`);
|
|
437
|
+
expect(result.exportPath).toBeDefined();
|
|
438
|
+
});
|
|
439
|
+
it('should handle special characters in session data', async () => {
|
|
440
|
+
const specialChars = 'Test with "quotes", \'apostrophes\', \n newlines, \t tabs, and unicode: 😀';
|
|
441
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'special_key', specialChars);
|
|
442
|
+
const result = await contextExportHandler({}, db, currentSessionId);
|
|
443
|
+
// Verify the exported file contains properly escaped special characters
|
|
444
|
+
const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
|
|
445
|
+
expect(exportData.contextItems[0].value).toBe(specialChars);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
describe('Integration Tests', () => {
|
|
449
|
+
it('should work with complete session data including all components', async () => {
|
|
450
|
+
// Create comprehensive test data
|
|
451
|
+
const itemIds = [];
|
|
452
|
+
// Add context items with various categories and priorities
|
|
453
|
+
['task', 'decision', 'progress', 'note', 'error', 'warning'].forEach((category, idx) => {
|
|
454
|
+
const itemId = (0, uuid_1.v4)();
|
|
455
|
+
itemIds.push(itemId);
|
|
456
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run(itemId, currentSessionId, `${category}_item`, `This is a ${category} item`, category, idx % 3 === 0 ? 'high' : idx % 3 === 1 ? 'normal' : 'low');
|
|
457
|
+
});
|
|
458
|
+
// Add file cache entries
|
|
459
|
+
['/src/index.ts', '/src/utils.ts', '/tests/test.spec.ts'].forEach(filePath => {
|
|
460
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, filePath, `Content of ${filePath}`, `hash_${filePath.replace(/\//g, '_')}`);
|
|
461
|
+
});
|
|
462
|
+
// Add checkpoints with linked items
|
|
463
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
464
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, description) VALUES (?, ?, ?, ?)').run(checkpointId, currentSessionId, 'Major Milestone', 'Checkpoint after completing major feature');
|
|
465
|
+
// Link some items to checkpoint
|
|
466
|
+
itemIds.slice(0, 3).forEach(itemId => {
|
|
467
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
|
|
468
|
+
});
|
|
469
|
+
// Export with full statistics
|
|
470
|
+
const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
|
|
471
|
+
// Verify comprehensive export
|
|
472
|
+
expect(result.statistics).toEqual({
|
|
473
|
+
items: 6,
|
|
474
|
+
files: 3,
|
|
475
|
+
checkpoints: 1,
|
|
476
|
+
size: expect.any(Number),
|
|
477
|
+
});
|
|
478
|
+
expect(result.content[0].text).toContain('Context Items: 6');
|
|
479
|
+
expect(result.content[0].text).toContain('Cached Files: 3');
|
|
480
|
+
expect(result.content[0].text).toContain('Checkpoints: 1');
|
|
481
|
+
// Verify exported data structure
|
|
482
|
+
const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
|
|
483
|
+
expect(exportData.contextItems).toHaveLength(6);
|
|
484
|
+
expect(exportData.fileCache).toHaveLength(3);
|
|
485
|
+
expect(exportData.checkpoints).toHaveLength(1);
|
|
486
|
+
expect(exportData.metadata).toBeDefined();
|
|
487
|
+
expect(exportData.metadata.totalSize).toBeGreaterThan(0);
|
|
488
|
+
});
|
|
489
|
+
it('should handle concurrent export requests gracefully', async () => {
|
|
490
|
+
// Add test data
|
|
491
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
|
|
492
|
+
// Simulate concurrent exports
|
|
493
|
+
const promises = Array(5)
|
|
494
|
+
.fill(null)
|
|
495
|
+
.map(() => contextExportHandler({ includeStats: true }, db, currentSessionId));
|
|
496
|
+
const results = await Promise.all(promises);
|
|
497
|
+
// All exports should succeed
|
|
498
|
+
results.forEach(result => {
|
|
499
|
+
expect(result.exportPath).toBeDefined();
|
|
500
|
+
expect(result.statistics.items).toBe(1);
|
|
501
|
+
});
|
|
502
|
+
// Clean up export files
|
|
503
|
+
results.forEach(result => {
|
|
504
|
+
if (result.exportPath && fs.existsSync(result.exportPath)) {
|
|
505
|
+
fs.unlinkSync(result.exportPath);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|