@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1281 -0
  4. package/bin/mcp-memory-keeper +54 -0
  5. package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
  6. package/dist/__tests__/e2e/server-e2e.test.js +341 -0
  7. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  8. package/dist/__tests__/helpers/test-server.js +92 -0
  9. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  10. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  11. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  12. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  13. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  14. package/dist/__tests__/integration/channels.test.js +376 -0
  15. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  16. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  17. package/dist/__tests__/integration/context-operations.test.js +243 -0
  18. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  19. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  20. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  21. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  22. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  23. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  24. package/dist/__tests__/integration/contextSearch.test.js +1054 -0
  25. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  26. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  27. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  28. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  29. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  30. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  31. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  32. package/dist/__tests__/integration/error-cases.test.js +411 -0
  33. package/dist/__tests__/integration/export-import.test.js +367 -0
  34. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  35. package/dist/__tests__/integration/file-operations.test.js +264 -0
  36. package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
  37. package/dist/__tests__/integration/git-integration.test.js +241 -0
  38. package/dist/__tests__/integration/index-tools.test.js +496 -0
  39. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  40. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  41. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  42. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  43. package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
  44. package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
  45. package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
  46. package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
  47. package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
  48. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  49. package/dist/__tests__/integration/migrations.test.js +528 -0
  50. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  51. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  52. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  53. package/dist/__tests__/integration/project-directory.test.js +291 -0
  54. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  55. package/dist/__tests__/integration/retention.test.js +513 -0
  56. package/dist/__tests__/integration/search.test.js +333 -0
  57. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  58. package/dist/__tests__/integration/server-initialization.test.js +305 -0
  59. package/dist/__tests__/integration/session-management.test.js +219 -0
  60. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  61. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  62. package/dist/__tests__/integration/summarization.test.js +308 -0
  63. package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
  64. package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
  65. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  66. package/dist/__tests__/security/input-validation.test.js +115 -0
  67. package/dist/__tests__/utils/agents.test.js +473 -0
  68. package/dist/__tests__/utils/database.test.js +177 -0
  69. package/dist/__tests__/utils/git.test.js +122 -0
  70. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  71. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  72. package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
  73. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  74. package/dist/__tests__/utils/token-limits.test.js +225 -0
  75. package/dist/__tests__/utils/tool-profiles.test.js +374 -0
  76. package/dist/__tests__/utils/validation.test.js +200 -0
  77. package/dist/__tests__/utils/vector-store.test.js +231 -0
  78. package/dist/handlers/contextWatchHandlers.js +206 -0
  79. package/dist/index.js +4425 -0
  80. package/dist/migrations/003_add_channels.js +174 -0
  81. package/dist/migrations/004_add_context_watch.js +151 -0
  82. package/dist/migrations/005_add_context_watch.js +98 -0
  83. package/dist/migrations/simplify-sharing.js +117 -0
  84. package/dist/repositories/BaseRepository.js +30 -0
  85. package/dist/repositories/CheckpointRepository.js +140 -0
  86. package/dist/repositories/ContextRepository.js +2017 -0
  87. package/dist/repositories/FileRepository.js +104 -0
  88. package/dist/repositories/RepositoryManager.js +62 -0
  89. package/dist/repositories/SessionRepository.js +66 -0
  90. package/dist/repositories/WatcherRepository.js +252 -0
  91. package/dist/repositories/index.js +15 -0
  92. package/dist/test-helpers/database-helper.js +128 -0
  93. package/dist/types/entities.js +3 -0
  94. package/dist/utils/agents.js +791 -0
  95. package/dist/utils/channels.js +150 -0
  96. package/dist/utils/database.js +780 -0
  97. package/dist/utils/feature-flags.js +476 -0
  98. package/dist/utils/git.js +145 -0
  99. package/dist/utils/knowledge-graph.js +264 -0
  100. package/dist/utils/migrationHealthCheck.js +373 -0
  101. package/dist/utils/migrations.js +452 -0
  102. package/dist/utils/retention.js +460 -0
  103. package/dist/utils/timestamps.js +112 -0
  104. package/dist/utils/token-limits.js +350 -0
  105. package/dist/utils/tool-profiles.js +242 -0
  106. package/dist/utils/validation.js +296 -0
  107. package/dist/utils/vector-store.js +247 -0
  108. package/examples/config.json +31 -0
  109. package/examples/project-directory-setup.md +114 -0
  110. package/package.json +85 -0
@@ -0,0 +1,264 @@
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 crypto = __importStar(require("crypto"));
41
+ const uuid_1 = require("uuid");
42
+ describe('File Operations Integration Tests', () => {
43
+ let dbManager;
44
+ let tempDbPath;
45
+ let tempFileDir;
46
+ let db;
47
+ let testSessionId;
48
+ beforeEach(() => {
49
+ tempDbPath = path.join(os.tmpdir(), `test-files-${Date.now()}.db`);
50
+ tempFileDir = path.join(os.tmpdir(), `test-files-${Date.now()}`);
51
+ dbManager = new database_1.DatabaseManager({
52
+ filename: tempDbPath,
53
+ maxSize: 10 * 1024 * 1024,
54
+ walMode: true,
55
+ });
56
+ db = dbManager.getDatabase();
57
+ // Create test directory
58
+ fs.mkdirSync(tempFileDir, { recursive: true });
59
+ // Create test session
60
+ testSessionId = (0, uuid_1.v4)();
61
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'File Test Session');
62
+ });
63
+ afterEach(() => {
64
+ dbManager.close();
65
+ try {
66
+ fs.unlinkSync(tempDbPath);
67
+ fs.unlinkSync(`${tempDbPath}-wal`);
68
+ fs.unlinkSync(`${tempDbPath}-shm`);
69
+ fs.rmSync(tempFileDir, { recursive: true, force: true });
70
+ }
71
+ catch (_e) {
72
+ // Ignore
73
+ }
74
+ });
75
+ describe('context_cache_file', () => {
76
+ it('should cache file content with hash', () => {
77
+ const filePath = path.join(tempFileDir, 'test.txt');
78
+ const content = 'This is test file content';
79
+ fs.writeFileSync(filePath, content);
80
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
81
+ const result = db
82
+ .prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)')
83
+ .run((0, uuid_1.v4)(), testSessionId, filePath, content, hash);
84
+ expect(result.changes).toBe(1);
85
+ const cached = db
86
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
87
+ .get(testSessionId, filePath);
88
+ expect(cached).toBeDefined();
89
+ expect(cached.content).toBe(content);
90
+ expect(cached.hash).toBe(hash);
91
+ });
92
+ it('should update existing file cache', () => {
93
+ const filePath = path.join(tempFileDir, 'update.txt');
94
+ const originalContent = 'Original content';
95
+ const updatedContent = 'Updated content';
96
+ // Cache original
97
+ const originalHash = crypto.createHash('sha256').update(originalContent).digest('hex');
98
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, originalContent, originalHash);
99
+ // Update cache
100
+ const updatedHash = crypto.createHash('sha256').update(updatedContent).digest('hex');
101
+ db.prepare('UPDATE file_cache SET content = ?, hash = ?, last_read = CURRENT_TIMESTAMP WHERE session_id = ? AND file_path = ?').run(updatedContent, updatedHash, testSessionId, filePath);
102
+ const cached = db
103
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
104
+ .get(testSessionId, filePath);
105
+ expect(cached.content).toBe(updatedContent);
106
+ expect(cached.hash).toBe(updatedHash);
107
+ });
108
+ it('should handle binary file content', () => {
109
+ const filePath = path.join(tempFileDir, 'binary.bin');
110
+ const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
111
+ fs.writeFileSync(filePath, binaryContent);
112
+ const base64Content = binaryContent.toString('base64');
113
+ const hash = crypto.createHash('sha256').update(binaryContent).digest('hex');
114
+ const result = db
115
+ .prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)')
116
+ .run((0, uuid_1.v4)(), testSessionId, filePath, base64Content, hash);
117
+ expect(result.changes).toBe(1);
118
+ const cached = db
119
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
120
+ .get(testSessionId, filePath);
121
+ expect(cached.content).toBe(base64Content);
122
+ // Verify we can decode back
123
+ const decoded = Buffer.from(cached.content, 'base64');
124
+ expect(decoded.equals(binaryContent)).toBe(true);
125
+ });
126
+ it('should handle very large files', () => {
127
+ const filePath = path.join(tempFileDir, 'large.txt');
128
+ const largeContent = 'x'.repeat(1024 * 1024); // 1MB
129
+ fs.writeFileSync(filePath, largeContent);
130
+ const hash = crypto.createHash('sha256').update(largeContent).digest('hex');
131
+ const result = db
132
+ .prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)')
133
+ .run((0, uuid_1.v4)(), testSessionId, filePath, largeContent, hash);
134
+ expect(result.changes).toBe(1);
135
+ const cached = db
136
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
137
+ .get(testSessionId, filePath);
138
+ expect(cached.content.length).toBe(1024 * 1024);
139
+ expect(cached.hash).toBe(hash);
140
+ });
141
+ it('should track last read timestamp', async () => {
142
+ const filePath = path.join(tempFileDir, 'timestamp.txt');
143
+ const content = 'Test content';
144
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, content, 'hash');
145
+ const cached = db
146
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
147
+ .get(testSessionId, filePath);
148
+ // Verify we have a last_read timestamp
149
+ expect(cached.last_read).toBeDefined();
150
+ const lastRead = new Date(cached.last_read);
151
+ expect(lastRead).toBeInstanceOf(Date);
152
+ // The timestamp should be a valid date (not NaN)
153
+ expect(lastRead.getTime()).not.toBeNaN();
154
+ // SQLite timestamps can have timezone issues, so we just verify it's reasonable
155
+ // (within 24 hours of now in either direction)
156
+ const now = Date.now();
157
+ const dayInMs = 24 * 60 * 60 * 1000;
158
+ expect(Math.abs(lastRead.getTime() - now)).toBeLessThan(dayInMs);
159
+ });
160
+ });
161
+ describe('context_file_changed', () => {
162
+ it('should detect no change when hash matches', () => {
163
+ const filePath = path.join(tempFileDir, 'unchanged.txt');
164
+ const content = 'Unchanged content';
165
+ fs.writeFileSync(filePath, content);
166
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
167
+ // Cache the file
168
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, content, hash);
169
+ // Check if changed (using same hash)
170
+ const cached = db
171
+ .prepare('SELECT hash FROM file_cache WHERE session_id = ? AND file_path = ?')
172
+ .get(testSessionId, filePath);
173
+ const currentHash = crypto.createHash('sha256').update(content).digest('hex');
174
+ expect(cached.hash).toBe(currentHash);
175
+ expect(cached.hash === currentHash).toBe(true); // Not changed
176
+ });
177
+ it('should detect change when hash differs', () => {
178
+ const filePath = path.join(tempFileDir, 'changed.txt');
179
+ const originalContent = 'Original content';
180
+ const newContent = 'Modified content';
181
+ const originalHash = crypto.createHash('sha256').update(originalContent).digest('hex');
182
+ // Cache original
183
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, originalContent, originalHash);
184
+ // Simulate file change
185
+ fs.writeFileSync(filePath, newContent);
186
+ const newHash = crypto.createHash('sha256').update(newContent).digest('hex');
187
+ const cached = db
188
+ .prepare('SELECT hash FROM file_cache WHERE session_id = ? AND file_path = ?')
189
+ .get(testSessionId, filePath);
190
+ expect(cached.hash).toBe(originalHash);
191
+ expect(cached.hash === newHash).toBe(false); // Changed
192
+ });
193
+ it('should handle file not in cache', () => {
194
+ const filePath = path.join(tempFileDir, 'notcached.txt');
195
+ const cached = db
196
+ .prepare('SELECT hash FROM file_cache WHERE session_id = ? AND file_path = ?')
197
+ .get(testSessionId, filePath);
198
+ expect(cached).toBeUndefined();
199
+ });
200
+ it('should handle file deletion', () => {
201
+ const filePath = path.join(tempFileDir, 'deleted.txt');
202
+ const content = 'To be deleted';
203
+ fs.writeFileSync(filePath, content);
204
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
205
+ // Cache the file
206
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, content, hash);
207
+ // Delete the file
208
+ fs.unlinkSync(filePath);
209
+ // Check existence
210
+ expect(fs.existsSync(filePath)).toBe(false);
211
+ // Cache still exists
212
+ const cached = db
213
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
214
+ .get(testSessionId, filePath);
215
+ expect(cached).toBeDefined();
216
+ expect(cached.content).toBe(content); // Can still retrieve content
217
+ });
218
+ it('should handle multiple files efficiently', () => {
219
+ const files = [];
220
+ // Create and cache multiple files
221
+ for (let i = 0; i < 10; i++) {
222
+ const filePath = path.join(tempFileDir, `file${i}.txt`);
223
+ const content = `Content for file ${i}`;
224
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
225
+ fs.writeFileSync(filePath, content);
226
+ files.push({ path: filePath, content, hash });
227
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, filePath, content, hash);
228
+ }
229
+ // Check all files
230
+ const cachedFiles = db
231
+ .prepare('SELECT file_path, hash FROM file_cache WHERE session_id = ?')
232
+ .all(testSessionId);
233
+ expect(cachedFiles).toHaveLength(10);
234
+ // Verify each file
235
+ cachedFiles.forEach((cached) => {
236
+ const original = files.find(f => f.path === cached.file_path);
237
+ expect(original).toBeDefined();
238
+ expect(cached.hash).toBe(original.hash);
239
+ });
240
+ });
241
+ });
242
+ describe('File operations with context', () => {
243
+ it('should link file cache with context items', () => {
244
+ const filePath = path.join(tempFileDir, 'linked.txt');
245
+ const content = 'Linked file content';
246
+ fs.writeFileSync(filePath, content);
247
+ // Cache file
248
+ const fileId = (0, uuid_1.v4)();
249
+ const hash = crypto.createHash('sha256').update(content).digest('hex');
250
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run(fileId, testSessionId, filePath, content, hash);
251
+ // Save related context
252
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'working_file', filePath, 'file_reference');
253
+ // Query both
254
+ const context = db
255
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
256
+ .get(testSessionId, 'working_file');
257
+ const file = db
258
+ .prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
259
+ .get(testSessionId, context.value);
260
+ expect(file).toBeDefined();
261
+ expect(file.content).toBe(content);
262
+ });
263
+ });
264
+ });
@@ -0,0 +1,251 @@
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 ContextRepository_1 = require("../../repositories/ContextRepository");
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs"));
42
+ const uuid_1 = require("uuid");
43
+ /**
44
+ * Tests for filterBySessionId parameter in queryEnhanced
45
+ *
46
+ * Bug: When user specifies sessionId in context_get, the API should return
47
+ * only items from that specific session (respecting privacy rules).
48
+ *
49
+ * Previous behavior: sessionId was only used for privacy checking, not filtering.
50
+ * All public items from ALL sessions were returned regardless of sessionId.
51
+ *
52
+ * Fixed behavior: When sessionId is specified, only items from that session
53
+ * are returned (public items always visible, private items only if current session).
54
+ */
55
+ (0, globals_1.describe)('filterBySessionId parameter in queryEnhanced', () => {
56
+ let dbManager;
57
+ let tempDbPath;
58
+ let db;
59
+ let contextRepo;
60
+ let sessionA;
61
+ let sessionB;
62
+ let sessionC;
63
+ (0, globals_1.beforeEach)(() => {
64
+ tempDbPath = path.join(os.tmpdir(), `test-filter-session-${Date.now()}.db`);
65
+ dbManager = new database_1.DatabaseManager({
66
+ filename: tempDbPath,
67
+ maxSize: 10 * 1024 * 1024,
68
+ walMode: true,
69
+ });
70
+ db = dbManager.getDatabase();
71
+ contextRepo = new ContextRepository_1.ContextRepository(dbManager);
72
+ // Create three test sessions
73
+ sessionA = (0, uuid_1.v4)();
74
+ sessionB = (0, uuid_1.v4)();
75
+ sessionC = (0, uuid_1.v4)();
76
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionA, 'Session A - Main');
77
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionB, 'Session B - Other');
78
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionC, 'Session C - Third');
79
+ // Create test items in different sessions
80
+ const testItems = [
81
+ // Session A items
82
+ {
83
+ id: (0, uuid_1.v4)(),
84
+ session_id: sessionA,
85
+ key: 'session-a-public-1',
86
+ value: 'Public item 1 in session A',
87
+ category: 'note',
88
+ priority: 'normal',
89
+ channel: 'main',
90
+ created_at: new Date().toISOString(),
91
+ is_private: 0,
92
+ },
93
+ {
94
+ id: (0, uuid_1.v4)(),
95
+ session_id: sessionA,
96
+ key: 'session-a-private-1',
97
+ value: 'Private item 1 in session A',
98
+ category: 'note',
99
+ priority: 'high',
100
+ channel: 'main',
101
+ created_at: new Date().toISOString(),
102
+ is_private: 1,
103
+ },
104
+ // Session B items
105
+ {
106
+ id: (0, uuid_1.v4)(),
107
+ session_id: sessionB,
108
+ key: 'session-b-public-1',
109
+ value: 'Public item 1 in session B',
110
+ category: 'task',
111
+ priority: 'normal',
112
+ channel: 'main',
113
+ created_at: new Date().toISOString(),
114
+ is_private: 0,
115
+ },
116
+ {
117
+ id: (0, uuid_1.v4)(),
118
+ session_id: sessionB,
119
+ key: 'session-b-private-1',
120
+ value: 'Private item 1 in session B',
121
+ category: 'task',
122
+ priority: 'normal',
123
+ channel: 'main',
124
+ created_at: new Date().toISOString(),
125
+ is_private: 1,
126
+ },
127
+ // Session C items
128
+ {
129
+ id: (0, uuid_1.v4)(),
130
+ session_id: sessionC,
131
+ key: 'session-c-public-1',
132
+ value: 'Public item 1 in session C',
133
+ category: 'reference',
134
+ priority: 'low',
135
+ channel: 'main',
136
+ created_at: new Date().toISOString(),
137
+ is_private: 0,
138
+ },
139
+ ];
140
+ const insertStmt = db.prepare(`
141
+ INSERT INTO context_items (id, session_id, key, value, category, priority, channel, created_at, is_private)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
143
+ `);
144
+ for (const item of testItems) {
145
+ insertStmt.run(item.id, item.session_id, item.key, item.value, item.category, item.priority, item.channel, item.created_at, item.is_private);
146
+ }
147
+ });
148
+ (0, globals_1.afterEach)(() => {
149
+ dbManager.close();
150
+ if (fs.existsSync(tempDbPath)) {
151
+ fs.unlinkSync(tempDbPath);
152
+ }
153
+ });
154
+ (0, globals_1.describe)('Without filterBySessionId (default behavior)', () => {
155
+ (0, globals_1.it)('should return all accessible items (public from all + private from current)', () => {
156
+ const result = contextRepo.queryEnhanced({
157
+ sessionId: sessionA, // Current session for privacy check
158
+ // No filterBySessionId - should return all accessible items
159
+ });
160
+ // Should see:
161
+ // - All public items (3 items from sessions A, B, C)
162
+ // - Private items from sessionA (1 item)
163
+ // Total: 4 items
164
+ (0, globals_1.expect)(result.items.length).toBe(4);
165
+ const publicItems = result.items.filter(item => item.is_private === 0);
166
+ const privateItems = result.items.filter(item => item.is_private === 1);
167
+ (0, globals_1.expect)(publicItems.length).toBe(3); // Public from A, B, C
168
+ (0, globals_1.expect)(privateItems.length).toBe(1); // Only private from A (current session)
169
+ (0, globals_1.expect)(privateItems[0].session_id).toBe(sessionA);
170
+ });
171
+ });
172
+ (0, globals_1.describe)('With filterBySessionId', () => {
173
+ (0, globals_1.it)('should return only items from the specified session', () => {
174
+ const result = contextRepo.queryEnhanced({
175
+ sessionId: sessionA, // Current session for privacy check
176
+ filterBySessionId: sessionA, // Filter to only session A
177
+ });
178
+ // Should only see items from session A
179
+ (0, globals_1.expect)(result.items.length).toBe(2); // 1 public + 1 private
180
+ (0, globals_1.expect)(result.items.every(item => item.session_id === sessionA)).toBe(true);
181
+ });
182
+ (0, globals_1.it)('should respect privacy when filtering another session', () => {
183
+ const result = contextRepo.queryEnhanced({
184
+ sessionId: sessionA, // Current session for privacy check
185
+ filterBySessionId: sessionB, // Filter to session B
186
+ });
187
+ // Should only see public items from session B
188
+ // (private items from B are not visible to A)
189
+ (0, globals_1.expect)(result.items.length).toBe(1);
190
+ (0, globals_1.expect)(result.items[0].key).toBe('session-b-public-1');
191
+ (0, globals_1.expect)(result.items[0].is_private).toBe(0);
192
+ });
193
+ (0, globals_1.it)('should see all items when filtering own session', () => {
194
+ const result = contextRepo.queryEnhanced({
195
+ sessionId: sessionA, // Current session
196
+ filterBySessionId: sessionA, // Filter to same session
197
+ });
198
+ // Should see both public and private items from session A
199
+ (0, globals_1.expect)(result.items.length).toBe(2);
200
+ const keys = result.items.map(i => i.key).sort();
201
+ (0, globals_1.expect)(keys).toEqual(['session-a-private-1', 'session-a-public-1']);
202
+ });
203
+ (0, globals_1.it)('should return empty when filtering session with only private items from another session', () => {
204
+ // Create a session with only private items
205
+ const privateOnlySession = (0, uuid_1.v4)();
206
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(privateOnlySession, 'Private Only Session');
207
+ db.prepare(`
208
+ INSERT INTO context_items (id, session_id, key, value, category, priority, channel, created_at, is_private)
209
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
210
+ `).run((0, uuid_1.v4)(), privateOnlySession, 'secret-item', 'This is a secret', 'note', 'normal', 'main', new Date().toISOString(), 1 // Private
211
+ );
212
+ const result = contextRepo.queryEnhanced({
213
+ sessionId: sessionA, // Current session (not privateOnlySession)
214
+ filterBySessionId: privateOnlySession, // Filter to private-only session
215
+ });
216
+ // Should return no items (the only item is private and belongs to another session)
217
+ (0, globals_1.expect)(result.items.length).toBe(0);
218
+ });
219
+ (0, globals_1.it)('should work with other filters combined', () => {
220
+ const result = contextRepo.queryEnhanced({
221
+ sessionId: sessionA,
222
+ filterBySessionId: sessionA,
223
+ category: 'note',
224
+ });
225
+ // Should return only note items from session A
226
+ (0, globals_1.expect)(result.items.length).toBe(2); // Both session A items are notes
227
+ (0, globals_1.expect)(result.items.every(item => item.category === 'note')).toBe(true);
228
+ });
229
+ });
230
+ (0, globals_1.describe)('Edge cases', () => {
231
+ (0, globals_1.it)('should handle non-existent session gracefully', () => {
232
+ const nonExistentSession = (0, uuid_1.v4)();
233
+ const result = contextRepo.queryEnhanced({
234
+ sessionId: sessionA,
235
+ filterBySessionId: nonExistentSession,
236
+ });
237
+ (0, globals_1.expect)(result.items.length).toBe(0);
238
+ (0, globals_1.expect)(result.totalCount).toBe(0);
239
+ });
240
+ (0, globals_1.it)('should handle undefined filterBySessionId same as not provided', () => {
241
+ const resultWithUndefined = contextRepo.queryEnhanced({
242
+ sessionId: sessionA,
243
+ filterBySessionId: undefined,
244
+ });
245
+ const resultWithout = contextRepo.queryEnhanced({
246
+ sessionId: sessionA,
247
+ });
248
+ (0, globals_1.expect)(resultWithUndefined.items.length).toBe(resultWithout.items.length);
249
+ });
250
+ });
251
+ });