@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,852 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const database_1 = require("../../utils/database");
|
|
37
|
+
const RepositoryManager_1 = require("../../repositories/RepositoryManager");
|
|
38
|
+
const database_test_helper_1 = require("../helpers/database-test-helper");
|
|
39
|
+
const timestamps_1 = require("../../utils/timestamps");
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const uuid_1 = require("uuid");
|
|
44
|
+
const timezone_safe_dates_1 = require("../utils/timezone-safe-dates");
|
|
45
|
+
describe('Context Diff Integration Tests', () => {
|
|
46
|
+
let dbManager;
|
|
47
|
+
let tempDbPath;
|
|
48
|
+
let db;
|
|
49
|
+
let testHelper;
|
|
50
|
+
let testSessionId;
|
|
51
|
+
let otherSessionId;
|
|
52
|
+
let repositories;
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
tempDbPath = path.join(os.tmpdir(), `test-context-diff-${Date.now()}.db`);
|
|
55
|
+
dbManager = new database_1.DatabaseManager({
|
|
56
|
+
filename: tempDbPath,
|
|
57
|
+
maxSize: 10 * 1024 * 1024,
|
|
58
|
+
walMode: true,
|
|
59
|
+
});
|
|
60
|
+
db = dbManager.getDatabase();
|
|
61
|
+
testHelper = new database_test_helper_1.DatabaseTestHelper(db);
|
|
62
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
63
|
+
// Create test sessions
|
|
64
|
+
testSessionId = (0, uuid_1.v4)();
|
|
65
|
+
otherSessionId = (0, uuid_1.v4)();
|
|
66
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Test Session');
|
|
67
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
|
|
68
|
+
});
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
dbManager.close();
|
|
71
|
+
try {
|
|
72
|
+
fs.unlinkSync(tempDbPath);
|
|
73
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
74
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
75
|
+
}
|
|
76
|
+
catch (_e) {
|
|
77
|
+
// Ignore
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
describe('Basic Diff Operations', () => {
|
|
81
|
+
it('should detect added items since timestamp', () => {
|
|
82
|
+
// TIMEZONE-SAFE: Use UTC-based date calculations
|
|
83
|
+
(0, timezone_safe_dates_1.validateTimezoneSafety)();
|
|
84
|
+
const baseTime = (0, timezone_safe_dates_1.createUTCDateByHours)(-2);
|
|
85
|
+
// Add items at different times
|
|
86
|
+
const oldItem = {
|
|
87
|
+
id: (0, uuid_1.v4)(),
|
|
88
|
+
key: 'old_item',
|
|
89
|
+
value: 'This existed before',
|
|
90
|
+
created_at: (0, timezone_safe_dates_1.createUTCDateByHours)(-2.1).toISOString(), // Slightly before base time
|
|
91
|
+
};
|
|
92
|
+
const newItem = {
|
|
93
|
+
id: (0, uuid_1.v4)(),
|
|
94
|
+
key: 'new_item',
|
|
95
|
+
value: 'This was added recently',
|
|
96
|
+
created_at: (0, timezone_safe_dates_1.createUTCDateByHours)(0).toISOString(), // Now
|
|
97
|
+
};
|
|
98
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(oldItem.id, testSessionId, oldItem.key, oldItem.value, (0, timestamps_1.toSQLiteTimestamp)(oldItem.created_at), (0, timestamps_1.toSQLiteTimestamp)(oldItem.created_at));
|
|
99
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(newItem.id, testSessionId, newItem.key, newItem.value, (0, timestamps_1.toSQLiteTimestamp)(newItem.created_at), (0, timestamps_1.toSQLiteTimestamp)(newItem.created_at));
|
|
100
|
+
// Query for items added since baseTime
|
|
101
|
+
const addedItems = db
|
|
102
|
+
.prepare(`
|
|
103
|
+
SELECT * FROM context_items
|
|
104
|
+
WHERE session_id = ? AND created_at > ?
|
|
105
|
+
ORDER BY created_at DESC
|
|
106
|
+
`)
|
|
107
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
108
|
+
expect(addedItems).toHaveLength(1);
|
|
109
|
+
expect(addedItems[0].key).toBe('new_item');
|
|
110
|
+
});
|
|
111
|
+
it('should detect modified items', () => {
|
|
112
|
+
const baseTime = new Date();
|
|
113
|
+
baseTime.setHours(baseTime.getHours() - 2);
|
|
114
|
+
// Create an item
|
|
115
|
+
const itemId = (0, uuid_1.v4)();
|
|
116
|
+
const createTime = (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 1000).toISOString());
|
|
117
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(itemId, testSessionId, 'changing_item', 'Original value', createTime, createTime);
|
|
118
|
+
// Update the item
|
|
119
|
+
const newUpdateTime = (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString());
|
|
120
|
+
db.prepare('UPDATE context_items SET value = ?, updated_at = ? WHERE id = ?').run('Modified value', newUpdateTime, itemId);
|
|
121
|
+
// Query for items modified since baseTime
|
|
122
|
+
const modifiedItems = db
|
|
123
|
+
.prepare(`
|
|
124
|
+
SELECT * FROM context_items
|
|
125
|
+
WHERE session_id = ?
|
|
126
|
+
AND created_at <= ?
|
|
127
|
+
AND updated_at > ?
|
|
128
|
+
ORDER BY updated_at DESC
|
|
129
|
+
`)
|
|
130
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
131
|
+
expect(modifiedItems).toHaveLength(1);
|
|
132
|
+
expect(modifiedItems[0].key).toBe('changing_item');
|
|
133
|
+
expect(modifiedItems[0].value).toBe('Modified value');
|
|
134
|
+
});
|
|
135
|
+
it('should handle no changes scenario', () => {
|
|
136
|
+
const baseTime = new Date();
|
|
137
|
+
// Add items before the base time
|
|
138
|
+
const oldTime = new Date(baseTime.getTime() - 60000).toISOString();
|
|
139
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Old value', (0, timestamps_1.toSQLiteTimestamp)(oldTime), (0, timestamps_1.toSQLiteTimestamp)(oldTime));
|
|
140
|
+
// Query for changes since baseTime (should be empty)
|
|
141
|
+
const addedItems = db
|
|
142
|
+
.prepare(`
|
|
143
|
+
SELECT * FROM context_items
|
|
144
|
+
WHERE session_id = ? AND created_at > ?
|
|
145
|
+
`)
|
|
146
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
147
|
+
const modifiedItems = db
|
|
148
|
+
.prepare(`
|
|
149
|
+
SELECT * FROM context_items
|
|
150
|
+
WHERE session_id = ?
|
|
151
|
+
AND created_at <= ?
|
|
152
|
+
AND updated_at > ?
|
|
153
|
+
`)
|
|
154
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
155
|
+
expect(addedItems).toHaveLength(0);
|
|
156
|
+
expect(modifiedItems).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('Checkpoint-based Diff', () => {
|
|
160
|
+
it('should compare against checkpoint using repository method', () => {
|
|
161
|
+
// Disable triggers to control timestamps precisely
|
|
162
|
+
testHelper.disableTimestampTriggers();
|
|
163
|
+
const checkpointTime = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago
|
|
164
|
+
// Create items at checkpoint time
|
|
165
|
+
const items = [
|
|
166
|
+
{ key: 'item1', value: 'Value 1' },
|
|
167
|
+
{ key: 'item2', value: 'Value 2' },
|
|
168
|
+
{ key: 'item3', value: 'Value 3' },
|
|
169
|
+
];
|
|
170
|
+
items.forEach(item => {
|
|
171
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
|
|
172
|
+
});
|
|
173
|
+
// Wait a bit to ensure timestamp difference
|
|
174
|
+
const afterCheckpoint = new Date(checkpointTime.getTime() + 2000); // 2 seconds later
|
|
175
|
+
// Make changes after checkpoint
|
|
176
|
+
// 1. Add new item
|
|
177
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item4', 'Value 4', (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()));
|
|
178
|
+
// 2. Modify existing item
|
|
179
|
+
db.prepare('UPDATE context_items SET value = ?, updated_at = ? WHERE session_id = ? AND key = ?').run('Modified Value 2', (0, timestamps_1.toSQLiteTimestamp)(afterCheckpoint.toISOString()), testSessionId, 'item2');
|
|
180
|
+
// 3. Delete an item
|
|
181
|
+
db.prepare('DELETE FROM context_items WHERE session_id = ? AND key = ?').run(testSessionId, 'item3');
|
|
182
|
+
// Re-enable triggers
|
|
183
|
+
testHelper.enableTimestampTriggers();
|
|
184
|
+
// Use repository method to get diff
|
|
185
|
+
const sinceTime = new Date(checkpointTime.getTime() + 1000); // 1 second after checkpoint
|
|
186
|
+
const diff = repositories.contexts.getDiff({
|
|
187
|
+
sessionId: testSessionId,
|
|
188
|
+
sinceTimestamp: sinceTime.toISOString(),
|
|
189
|
+
});
|
|
190
|
+
expect(diff.added).toHaveLength(1);
|
|
191
|
+
expect(diff.added[0].key).toBe('item4');
|
|
192
|
+
expect(diff.modified).toHaveLength(1);
|
|
193
|
+
expect(diff.modified[0].key).toBe('item2');
|
|
194
|
+
expect(diff.modified[0].value).toBe('Modified Value 2');
|
|
195
|
+
// Note: deleted items are only detected via checkpoint comparison, not timestamp diff
|
|
196
|
+
});
|
|
197
|
+
it('should compare against checkpoint', () => {
|
|
198
|
+
// Create initial state
|
|
199
|
+
const checkpointTime = new Date();
|
|
200
|
+
const items = [
|
|
201
|
+
{ key: 'item1', value: 'Value 1' },
|
|
202
|
+
{ key: 'item2', value: 'Value 2' },
|
|
203
|
+
{ key: 'item3', value: 'Value 3' },
|
|
204
|
+
];
|
|
205
|
+
const itemIds = [];
|
|
206
|
+
items.forEach(item => {
|
|
207
|
+
const id = (0, uuid_1.v4)();
|
|
208
|
+
itemIds.push(id);
|
|
209
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, testSessionId, item.key, item.value, (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
|
|
210
|
+
});
|
|
211
|
+
// Create checkpoint slightly after items (to ensure proper comparison)
|
|
212
|
+
const actualCheckpointTime = new Date(checkpointTime.getTime() + 1000); // 1 second later
|
|
213
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
214
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'test-checkpoint', (0, timestamps_1.toSQLiteTimestamp)(actualCheckpointTime.toISOString()));
|
|
215
|
+
// Link items to checkpoint
|
|
216
|
+
itemIds.forEach(itemId => {
|
|
217
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
|
|
218
|
+
});
|
|
219
|
+
// Make changes after checkpoint
|
|
220
|
+
// 1. Add new item
|
|
221
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item4', 'Value 4');
|
|
222
|
+
// Get checkpoint items BEFORE making changes
|
|
223
|
+
const checkpointItemsBefore = db
|
|
224
|
+
.prepare(`
|
|
225
|
+
SELECT ci.* FROM context_items ci
|
|
226
|
+
JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
|
|
227
|
+
WHERE cpi.checkpoint_id = ?
|
|
228
|
+
`)
|
|
229
|
+
.all(checkpointId);
|
|
230
|
+
// Store checkpoint state
|
|
231
|
+
const checkpointState = new Map(checkpointItemsBefore.map((item) => [item.key, item]));
|
|
232
|
+
// 2. Modify existing item
|
|
233
|
+
db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ? AND key = ?').run('Modified Value 2', testSessionId, 'item2');
|
|
234
|
+
// 3. Delete an item
|
|
235
|
+
db.prepare('DELETE FROM context_items WHERE session_id = ? AND key = ?').run(testSessionId, 'item3');
|
|
236
|
+
// Get current items after changes
|
|
237
|
+
const currentItems = db
|
|
238
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ?')
|
|
239
|
+
.all(testSessionId);
|
|
240
|
+
// Calculate diff using stored checkpoint state
|
|
241
|
+
const checkpointKeys = new Set(checkpointState.keys());
|
|
242
|
+
const currentKeys = new Set(currentItems.map((i) => i.key));
|
|
243
|
+
// Added: in current but not in checkpoint
|
|
244
|
+
const added = currentItems.filter((i) => !checkpointKeys.has(i.key));
|
|
245
|
+
expect(added).toHaveLength(1);
|
|
246
|
+
expect(added[0].key).toBe('item4');
|
|
247
|
+
// Modified: in both but values differ
|
|
248
|
+
const modified = currentItems.filter((i) => {
|
|
249
|
+
const checkpointItem = checkpointState.get(i.key);
|
|
250
|
+
return checkpointItem && checkpointItem.value !== i.value;
|
|
251
|
+
});
|
|
252
|
+
expect(modified).toHaveLength(1);
|
|
253
|
+
expect(modified[0].key).toBe('item2');
|
|
254
|
+
// Deleted: in checkpoint but not in current
|
|
255
|
+
const deleted = Array.from(checkpointKeys)
|
|
256
|
+
.filter(key => !currentKeys.has(key))
|
|
257
|
+
.map(key => checkpointState.get(key));
|
|
258
|
+
expect(deleted).toHaveLength(1);
|
|
259
|
+
expect(deleted[0].key).toBe('item3');
|
|
260
|
+
});
|
|
261
|
+
it('should handle non-existent checkpoint', () => {
|
|
262
|
+
const checkpoint = db
|
|
263
|
+
.prepare('SELECT * FROM checkpoints WHERE name = ?')
|
|
264
|
+
.get('non-existent-checkpoint');
|
|
265
|
+
expect(checkpoint).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('Relative Time Parsing', () => {
|
|
269
|
+
it('should parse "2 hours ago"', () => {
|
|
270
|
+
const now = new Date();
|
|
271
|
+
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
|
272
|
+
// Add items at different times
|
|
273
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'recent_item', 'Added 1 hour ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 60 * 60 * 1000).toISOString()));
|
|
274
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Added 3 hours ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(now.getTime() - 3 * 60 * 60 * 1000).toISOString()));
|
|
275
|
+
// Query items added since "2 hours ago"
|
|
276
|
+
const items = db
|
|
277
|
+
.prepare(`
|
|
278
|
+
SELECT * FROM context_items
|
|
279
|
+
WHERE session_id = ? AND created_at > ?
|
|
280
|
+
ORDER BY created_at DESC
|
|
281
|
+
`)
|
|
282
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(twoHoursAgo.toISOString()));
|
|
283
|
+
expect(items).toHaveLength(1);
|
|
284
|
+
expect(items[0].key).toBe('recent_item');
|
|
285
|
+
});
|
|
286
|
+
it('should parse "yesterday"', () => {
|
|
287
|
+
const now = new Date();
|
|
288
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
289
|
+
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
|
|
290
|
+
// Add items at different times
|
|
291
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'today_item', 'Added today', (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date().toISOString()));
|
|
292
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'yesterday_item', 'Added yesterday', (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() + 12 * 60 * 60 * 1000).toISOString()), // Noon yesterday
|
|
293
|
+
(0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() + 12 * 60 * 60 * 1000).toISOString()));
|
|
294
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old_item', 'Added 2 days ago', (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(yesterday.getTime() - 24 * 60 * 60 * 1000).toISOString()));
|
|
295
|
+
// Query items added since yesterday
|
|
296
|
+
const items = db
|
|
297
|
+
.prepare(`
|
|
298
|
+
SELECT * FROM context_items
|
|
299
|
+
WHERE session_id = ? AND created_at >= ?
|
|
300
|
+
ORDER BY created_at DESC
|
|
301
|
+
`)
|
|
302
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(yesterday.toISOString()));
|
|
303
|
+
expect(items).toHaveLength(2);
|
|
304
|
+
expect(items.map((i) => i.key)).toContain('today_item');
|
|
305
|
+
expect(items.map((i) => i.key)).toContain('yesterday_item');
|
|
306
|
+
expect(items.map((i) => i.key)).not.toContain('old_item');
|
|
307
|
+
});
|
|
308
|
+
it('should parse "3 days ago"', () => {
|
|
309
|
+
const now = new Date();
|
|
310
|
+
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000);
|
|
311
|
+
// Add items
|
|
312
|
+
for (let i = 0; i < 5; i++) {
|
|
313
|
+
const daysAgo = i;
|
|
314
|
+
const itemTime = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
315
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${daysAgo}d_ago`, `Added ${daysAgo} days ago`, (0, timestamps_1.toSQLiteTimestamp)(itemTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(itemTime.toISOString()));
|
|
316
|
+
}
|
|
317
|
+
// Query items since 3 days ago
|
|
318
|
+
const items = db
|
|
319
|
+
.prepare(`
|
|
320
|
+
SELECT * FROM context_items
|
|
321
|
+
WHERE session_id = ? AND created_at > ?
|
|
322
|
+
ORDER BY created_at DESC
|
|
323
|
+
`)
|
|
324
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(threeDaysAgo.toISOString()));
|
|
325
|
+
expect(items).toHaveLength(3); // 0, 1, 2 days ago
|
|
326
|
+
expect(items.map((i) => i.key)).toContain('item_0d_ago');
|
|
327
|
+
expect(items.map((i) => i.key)).toContain('item_1d_ago');
|
|
328
|
+
expect(items.map((i) => i.key)).toContain('item_2d_ago');
|
|
329
|
+
expect(items.map((i) => i.key)).not.toContain('item_3d_ago');
|
|
330
|
+
expect(items.map((i) => i.key)).not.toContain('item_4d_ago');
|
|
331
|
+
});
|
|
332
|
+
it('should handle invalid relative time formats', () => {
|
|
333
|
+
// Test that invalid formats don't crash
|
|
334
|
+
const invalidFormats = [
|
|
335
|
+
'invalid time',
|
|
336
|
+
'2 minutes ago', // Not supported
|
|
337
|
+
'next week',
|
|
338
|
+
'',
|
|
339
|
+
null,
|
|
340
|
+
];
|
|
341
|
+
invalidFormats.forEach(format => {
|
|
342
|
+
expect(() => {
|
|
343
|
+
db.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?').all(testSessionId, format);
|
|
344
|
+
}).not.toThrow();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
describe('Filtering Options', () => {
|
|
349
|
+
beforeEach(() => {
|
|
350
|
+
const baseTime = new Date();
|
|
351
|
+
baseTime.setHours(baseTime.getHours() - 2);
|
|
352
|
+
// Create diverse items for filtering tests
|
|
353
|
+
const items = [
|
|
354
|
+
{
|
|
355
|
+
key: 'task_new_high',
|
|
356
|
+
value: 'New high priority task',
|
|
357
|
+
category: 'task',
|
|
358
|
+
priority: 'high',
|
|
359
|
+
channel: 'main',
|
|
360
|
+
created_at: new Date().toISOString(),
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
key: 'task_old_normal',
|
|
364
|
+
value: 'Old normal priority task',
|
|
365
|
+
category: 'task',
|
|
366
|
+
priority: 'normal',
|
|
367
|
+
channel: 'main',
|
|
368
|
+
created_at: new Date(baseTime.getTime() - 1000).toISOString(),
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
key: 'note_new_low',
|
|
372
|
+
value: 'New low priority note',
|
|
373
|
+
category: 'note',
|
|
374
|
+
priority: 'low',
|
|
375
|
+
channel: 'feature/docs',
|
|
376
|
+
created_at: new Date().toISOString(),
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
key: 'decision_modified',
|
|
380
|
+
value: 'Modified decision',
|
|
381
|
+
category: 'decision',
|
|
382
|
+
priority: 'high',
|
|
383
|
+
channel: 'main',
|
|
384
|
+
created_at: new Date(baseTime.getTime() - 2000).toISOString(),
|
|
385
|
+
updated_at: new Date().toISOString(),
|
|
386
|
+
},
|
|
387
|
+
];
|
|
388
|
+
items.forEach(item => {
|
|
389
|
+
db.prepare(`INSERT INTO context_items
|
|
390
|
+
(id, session_id, key, value, category, priority, channel, created_at, updated_at)
|
|
391
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority, item.channel, (0, timestamps_1.toSQLiteTimestamp)(item.created_at), (0, timestamps_1.toSQLiteTimestamp)(item.updated_at || item.created_at));
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
it('should filter diff by category', () => {
|
|
395
|
+
const baseTime = new Date();
|
|
396
|
+
baseTime.setHours(baseTime.getHours() - 2);
|
|
397
|
+
// Get added items filtered by category
|
|
398
|
+
const addedTasks = db
|
|
399
|
+
.prepare(`
|
|
400
|
+
SELECT * FROM context_items
|
|
401
|
+
WHERE session_id = ?
|
|
402
|
+
AND created_at > ?
|
|
403
|
+
AND category = ?
|
|
404
|
+
ORDER BY created_at DESC
|
|
405
|
+
`)
|
|
406
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'task');
|
|
407
|
+
expect(addedTasks).toHaveLength(1);
|
|
408
|
+
expect(addedTasks[0].key).toBe('task_new_high');
|
|
409
|
+
});
|
|
410
|
+
it('should filter diff by channel', () => {
|
|
411
|
+
const baseTime = new Date();
|
|
412
|
+
baseTime.setHours(baseTime.getHours() - 2);
|
|
413
|
+
// Get all changes in 'main' channel
|
|
414
|
+
const mainChannelAdded = db
|
|
415
|
+
.prepare(`
|
|
416
|
+
SELECT * FROM context_items
|
|
417
|
+
WHERE session_id = ?
|
|
418
|
+
AND created_at > ?
|
|
419
|
+
AND channel = ?
|
|
420
|
+
ORDER BY created_at DESC
|
|
421
|
+
`)
|
|
422
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'main');
|
|
423
|
+
expect(mainChannelAdded).toHaveLength(1);
|
|
424
|
+
expect(mainChannelAdded[0].key).toBe('task_new_high');
|
|
425
|
+
});
|
|
426
|
+
it('should filter diff by multiple channels', () => {
|
|
427
|
+
const baseTime = new Date();
|
|
428
|
+
baseTime.setHours(baseTime.getHours() - 2);
|
|
429
|
+
const channels = ['main', 'feature/docs'];
|
|
430
|
+
const placeholders = channels.map(() => '?').join(',');
|
|
431
|
+
const multiChannelItems = db
|
|
432
|
+
.prepare(`
|
|
433
|
+
SELECT * FROM context_items
|
|
434
|
+
WHERE session_id = ?
|
|
435
|
+
AND created_at > ?
|
|
436
|
+
AND channel IN (${placeholders})
|
|
437
|
+
ORDER BY created_at DESC
|
|
438
|
+
`)
|
|
439
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), ...channels);
|
|
440
|
+
expect(multiChannelItems).toHaveLength(2);
|
|
441
|
+
expect(multiChannelItems.map((i) => i.key)).toContain('task_new_high');
|
|
442
|
+
expect(multiChannelItems.map((i) => i.key)).toContain('note_new_low');
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
describe('Include Values Option', () => {
|
|
446
|
+
it('should include full values when requested', () => {
|
|
447
|
+
const longValue = 'A'.repeat(1000);
|
|
448
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'long_item', longValue);
|
|
449
|
+
const item = db
|
|
450
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
|
|
451
|
+
.get(testSessionId, 'long_item');
|
|
452
|
+
expect(item.value).toBe(longValue);
|
|
453
|
+
expect(item.value.length).toBe(1000);
|
|
454
|
+
});
|
|
455
|
+
it('should be able to exclude values for summary', () => {
|
|
456
|
+
// Add items
|
|
457
|
+
for (let i = 0; i < 5; i++) {
|
|
458
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i}`, `Value ${i} with lots of text...`);
|
|
459
|
+
}
|
|
460
|
+
// Query without values (just keys and metadata)
|
|
461
|
+
const items = db
|
|
462
|
+
.prepare('SELECT id, session_id, key, category, priority, channel, created_at FROM context_items WHERE session_id = ?')
|
|
463
|
+
.all(testSessionId);
|
|
464
|
+
expect(items).toHaveLength(5);
|
|
465
|
+
items.forEach((item) => {
|
|
466
|
+
expect(item).not.toHaveProperty('value');
|
|
467
|
+
expect(item).toHaveProperty('key');
|
|
468
|
+
expect(item).toHaveProperty('created_at');
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
describe('Privacy and Session Boundaries', () => {
|
|
473
|
+
it('should respect privacy boundaries in diff', () => {
|
|
474
|
+
const baseTime = new Date();
|
|
475
|
+
baseTime.setHours(baseTime.getHours() - 1);
|
|
476
|
+
// Add items to both sessions
|
|
477
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_public', 'Public item', 0);
|
|
478
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_private', 'Private item', 1);
|
|
479
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_public', 'Other public', 0);
|
|
480
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, is_private) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_private', 'Other private', 1);
|
|
481
|
+
// Query from testSessionId perspective
|
|
482
|
+
const visibleItems = db
|
|
483
|
+
.prepare(`
|
|
484
|
+
SELECT * FROM context_items
|
|
485
|
+
WHERE created_at > ?
|
|
486
|
+
AND (is_private = 0 OR session_id = ?)
|
|
487
|
+
ORDER BY created_at DESC
|
|
488
|
+
`)
|
|
489
|
+
.all((0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), testSessionId);
|
|
490
|
+
expect(visibleItems.map((i) => i.key)).toContain('my_public');
|
|
491
|
+
expect(visibleItems.map((i) => i.key)).toContain('my_private');
|
|
492
|
+
expect(visibleItems.map((i) => i.key)).toContain('other_public');
|
|
493
|
+
expect(visibleItems.map((i) => i.key)).not.toContain('other_private');
|
|
494
|
+
});
|
|
495
|
+
it('should only show session-specific diff by default', () => {
|
|
496
|
+
const baseTime = new Date();
|
|
497
|
+
baseTime.setHours(baseTime.getHours() - 1);
|
|
498
|
+
// Add items to different sessions
|
|
499
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'my_item', 'My value');
|
|
500
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_item', 'Other value');
|
|
501
|
+
// Query for specific session
|
|
502
|
+
const sessionItems = db
|
|
503
|
+
.prepare(`
|
|
504
|
+
SELECT * FROM context_items
|
|
505
|
+
WHERE session_id = ? AND created_at > ?
|
|
506
|
+
`)
|
|
507
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
508
|
+
expect(sessionItems).toHaveLength(1);
|
|
509
|
+
expect(sessionItems[0].key).toBe('my_item');
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
describe('Deleted Items Detection', () => {
|
|
513
|
+
it('should detect deleted items using checkpoint comparison', () => {
|
|
514
|
+
// Create initial items
|
|
515
|
+
const item1Id = (0, uuid_1.v4)();
|
|
516
|
+
const item2Id = (0, uuid_1.v4)();
|
|
517
|
+
const item3Id = (0, uuid_1.v4)();
|
|
518
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item1Id, testSessionId, 'keep_item', 'Will remain');
|
|
519
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item2Id, testSessionId, 'delete_item1', 'Will be deleted');
|
|
520
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(item3Id, testSessionId, 'delete_item2', 'Will also be deleted');
|
|
521
|
+
// Create checkpoint
|
|
522
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
523
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, testSessionId, 'before-deletion');
|
|
524
|
+
// Link all items to checkpoint
|
|
525
|
+
[item1Id, item2Id, item3Id].forEach(itemId => {
|
|
526
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
|
|
527
|
+
});
|
|
528
|
+
// Get checkpoint items BEFORE deletion
|
|
529
|
+
const checkpointItemsBefore = db
|
|
530
|
+
.prepare(`
|
|
531
|
+
SELECT ci.key FROM context_items ci
|
|
532
|
+
JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
|
|
533
|
+
WHERE cpi.checkpoint_id = ?
|
|
534
|
+
`)
|
|
535
|
+
.all(checkpointId);
|
|
536
|
+
// Store keys before deletion
|
|
537
|
+
const keysBeforeDeletion = new Set(checkpointItemsBefore.map((i) => i.key));
|
|
538
|
+
// Delete some items
|
|
539
|
+
db.prepare('DELETE FROM context_items WHERE session_id = ? AND key IN (?, ?)').run(testSessionId, 'delete_item1', 'delete_item2');
|
|
540
|
+
// Get current items after deletion
|
|
541
|
+
const currentItems = db
|
|
542
|
+
.prepare('SELECT key FROM context_items WHERE session_id = ?')
|
|
543
|
+
.all(testSessionId);
|
|
544
|
+
const currentKeys = new Set(currentItems.map((i) => i.key));
|
|
545
|
+
// Find deleted items by comparing before and after
|
|
546
|
+
const deletedKeys = Array.from(keysBeforeDeletion).filter(key => !currentKeys.has(key));
|
|
547
|
+
expect(deletedKeys).toHaveLength(2);
|
|
548
|
+
expect(deletedKeys).toContain('delete_item1');
|
|
549
|
+
expect(deletedKeys).toContain('delete_item2');
|
|
550
|
+
expect(deletedKeys).not.toContain('keep_item');
|
|
551
|
+
});
|
|
552
|
+
it('should handle deletion time tracking if available', () => {
|
|
553
|
+
// Create audit log table for tracking deletions (if implemented)
|
|
554
|
+
// This is a potential enhancement for the actual implementation
|
|
555
|
+
db.prepare(`
|
|
556
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
557
|
+
id TEXT PRIMARY KEY,
|
|
558
|
+
session_id TEXT NOT NULL,
|
|
559
|
+
action TEXT NOT NULL,
|
|
560
|
+
item_key TEXT,
|
|
561
|
+
timestamp TEXT DEFAULT CURRENT_TIMESTAMP
|
|
562
|
+
)
|
|
563
|
+
`).run();
|
|
564
|
+
const itemId = (0, uuid_1.v4)();
|
|
565
|
+
const itemKey = 'tracked_item';
|
|
566
|
+
// Create and then delete an item
|
|
567
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, testSessionId, itemKey, 'Will be deleted with tracking');
|
|
568
|
+
// Delete and log
|
|
569
|
+
const deleteTime = new Date().toISOString();
|
|
570
|
+
db.prepare('DELETE FROM context_items WHERE id = ?').run(itemId);
|
|
571
|
+
db.prepare('INSERT INTO audit_log (id, session_id, action, item_key, timestamp) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'delete', itemKey, deleteTime);
|
|
572
|
+
// Query audit log for deletions
|
|
573
|
+
const deletions = db
|
|
574
|
+
.prepare(`
|
|
575
|
+
SELECT * FROM audit_log
|
|
576
|
+
WHERE session_id = ? AND action = 'delete'
|
|
577
|
+
ORDER BY timestamp DESC
|
|
578
|
+
`)
|
|
579
|
+
.all(testSessionId);
|
|
580
|
+
expect(deletions).toHaveLength(1);
|
|
581
|
+
expect(deletions[0].item_key).toBe(itemKey);
|
|
582
|
+
expect(deletions[0].timestamp).toBe(deleteTime);
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
describe('Edge Cases', () => {
|
|
586
|
+
it('should handle future timestamps gracefully', () => {
|
|
587
|
+
const futureTime = new Date();
|
|
588
|
+
futureTime.setDate(futureTime.getDate() + 1);
|
|
589
|
+
// Add current item
|
|
590
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'current_item', 'Current value');
|
|
591
|
+
// Query with future timestamp (should return nothing)
|
|
592
|
+
const items = db
|
|
593
|
+
.prepare(`
|
|
594
|
+
SELECT * FROM context_items
|
|
595
|
+
WHERE session_id = ? AND created_at > ?
|
|
596
|
+
`)
|
|
597
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(futureTime.toISOString()));
|
|
598
|
+
expect(items).toHaveLength(0);
|
|
599
|
+
});
|
|
600
|
+
it('should handle very old timestamps', () => {
|
|
601
|
+
const veryOldTime = (0, timestamps_1.toSQLiteTimestamp)(new Date('1970-01-01').toISOString());
|
|
602
|
+
// Add items
|
|
603
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item1', 'Value 1');
|
|
604
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'item2', 'Value 2');
|
|
605
|
+
// Query since very old time (should return all items)
|
|
606
|
+
const items = db
|
|
607
|
+
.prepare(`
|
|
608
|
+
SELECT * FROM context_items
|
|
609
|
+
WHERE session_id = ? AND created_at > ?
|
|
610
|
+
`)
|
|
611
|
+
.all(testSessionId, veryOldTime);
|
|
612
|
+
expect(items).toHaveLength(2);
|
|
613
|
+
});
|
|
614
|
+
it('should handle invalid checkpoint names', () => {
|
|
615
|
+
const checkpoint = db
|
|
616
|
+
.prepare('SELECT * FROM checkpoints WHERE session_id = ? AND name = ?')
|
|
617
|
+
.get(testSessionId, 'non-existent-checkpoint');
|
|
618
|
+
expect(checkpoint).toBeUndefined();
|
|
619
|
+
});
|
|
620
|
+
it('should handle empty diff results', () => {
|
|
621
|
+
const baseTime = new Date().toISOString();
|
|
622
|
+
// Query for changes (should be empty)
|
|
623
|
+
const added = db
|
|
624
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at > ?')
|
|
625
|
+
.all(testSessionId, baseTime);
|
|
626
|
+
const modified = db
|
|
627
|
+
.prepare(`
|
|
628
|
+
SELECT * FROM context_items
|
|
629
|
+
WHERE session_id = ?
|
|
630
|
+
AND created_at <= ?
|
|
631
|
+
AND updated_at > ?
|
|
632
|
+
`)
|
|
633
|
+
.all(testSessionId, baseTime, baseTime);
|
|
634
|
+
expect(added).toHaveLength(0);
|
|
635
|
+
expect(modified).toHaveLength(0);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
describe('Performance with Large Datasets', () => {
|
|
639
|
+
it('should efficiently diff large numbers of changes', () => {
|
|
640
|
+
const baseTime = new Date();
|
|
641
|
+
baseTime.setMinutes(baseTime.getMinutes() - 30);
|
|
642
|
+
// Create many items before base time
|
|
643
|
+
for (let i = 0; i < 500; i++) {
|
|
644
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `old_item_${i}`, `Old value ${i}`, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
|
|
645
|
+
}
|
|
646
|
+
// Create many new items
|
|
647
|
+
for (let i = 0; i < 500; i++) {
|
|
648
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `new_item_${i}`, `New value ${i}`);
|
|
649
|
+
}
|
|
650
|
+
const start = Date.now();
|
|
651
|
+
// Query for changes
|
|
652
|
+
const added = db
|
|
653
|
+
.prepare(`
|
|
654
|
+
SELECT COUNT(*) as count FROM context_items
|
|
655
|
+
WHERE session_id = ? AND created_at > ?
|
|
656
|
+
`)
|
|
657
|
+
.get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
658
|
+
const duration = Date.now() - start;
|
|
659
|
+
expect(added.count).toBe(500);
|
|
660
|
+
expect(duration).toBeLessThan(100); // Should be fast
|
|
661
|
+
});
|
|
662
|
+
it('should paginate large diff results', () => {
|
|
663
|
+
const baseTime = new Date();
|
|
664
|
+
baseTime.setMinutes(baseTime.getMinutes() - 30);
|
|
665
|
+
// Create many new items
|
|
666
|
+
for (let i = 0; i < 100; i++) {
|
|
667
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `item_${i.toString().padStart(3, '0')}`, `Value ${i}`);
|
|
668
|
+
}
|
|
669
|
+
// Get paginated results
|
|
670
|
+
const page1 = db
|
|
671
|
+
.prepare(`
|
|
672
|
+
SELECT * FROM context_items
|
|
673
|
+
WHERE session_id = ? AND created_at > ?
|
|
674
|
+
ORDER BY key ASC
|
|
675
|
+
LIMIT 20 OFFSET 0
|
|
676
|
+
`)
|
|
677
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
678
|
+
const page2 = db
|
|
679
|
+
.prepare(`
|
|
680
|
+
SELECT * FROM context_items
|
|
681
|
+
WHERE session_id = ? AND created_at > ?
|
|
682
|
+
ORDER BY key ASC
|
|
683
|
+
LIMIT 20 OFFSET 20
|
|
684
|
+
`)
|
|
685
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
686
|
+
expect(page1).toHaveLength(20);
|
|
687
|
+
expect(page2).toHaveLength(20);
|
|
688
|
+
expect(page1[0].key).toBe('item_000');
|
|
689
|
+
expect(page1[19].key).toBe('item_019');
|
|
690
|
+
expect(page2[0].key).toBe('item_020');
|
|
691
|
+
expect(page2[19].key).toBe('item_039');
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
describe('Complex Scenarios', () => {
|
|
695
|
+
it('should handle item recreation (delete then add with same key)', () => {
|
|
696
|
+
const checkpointTime = new Date();
|
|
697
|
+
// Create initial item
|
|
698
|
+
const originalId = (0, uuid_1.v4)();
|
|
699
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(originalId, testSessionId, 'recreated_item', 'Original value', (0, timestamps_1.toSQLiteTimestamp)(new Date(checkpointTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(checkpointTime.getTime() - 60000).toISOString()));
|
|
700
|
+
// Create checkpoint
|
|
701
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
702
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'before-recreation', (0, timestamps_1.toSQLiteTimestamp)(checkpointTime.toISOString()));
|
|
703
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, originalId);
|
|
704
|
+
// Get checkpoint item BEFORE deletion
|
|
705
|
+
const checkpointItemBefore = db
|
|
706
|
+
.prepare(`
|
|
707
|
+
SELECT ci.* FROM context_items ci
|
|
708
|
+
JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
|
|
709
|
+
WHERE cpi.checkpoint_id = ? AND ci.key = ?
|
|
710
|
+
`)
|
|
711
|
+
.get(checkpointId, 'recreated_item');
|
|
712
|
+
// Store the original data
|
|
713
|
+
const originalData = { ...checkpointItemBefore };
|
|
714
|
+
// Delete the item
|
|
715
|
+
db.prepare('DELETE FROM context_items WHERE id = ?').run(originalId);
|
|
716
|
+
// Recreate with same key but different value
|
|
717
|
+
const newId = (0, uuid_1.v4)();
|
|
718
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(newId, testSessionId, 'recreated_item', 'New value after recreation');
|
|
719
|
+
// Get current item
|
|
720
|
+
const currentItem = db
|
|
721
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
|
|
722
|
+
.get(testSessionId, 'recreated_item');
|
|
723
|
+
// Should be treated as modified (different id, different value)
|
|
724
|
+
expect(originalData).toBeDefined();
|
|
725
|
+
expect(currentItem).toBeDefined();
|
|
726
|
+
expect(originalData.id).not.toBe(currentItem.id);
|
|
727
|
+
expect(originalData.value).not.toBe(currentItem.value);
|
|
728
|
+
});
|
|
729
|
+
it('should handle mixed changes across categories and channels', () => {
|
|
730
|
+
const baseTime = new Date();
|
|
731
|
+
baseTime.setHours(baseTime.getHours() - 1);
|
|
732
|
+
// Create diverse changes
|
|
733
|
+
const changes = [
|
|
734
|
+
// Added items
|
|
735
|
+
{ action: 'add', key: 'task_new_1', category: 'task', channel: 'main' },
|
|
736
|
+
{ action: 'add', key: 'note_new_1', category: 'note', channel: 'feature/ui' },
|
|
737
|
+
// Modified items (created before, updated after)
|
|
738
|
+
{ action: 'modify', key: 'task_mod_1', category: 'task', channel: 'main' },
|
|
739
|
+
{ action: 'modify', key: 'decision_mod_1', category: 'decision', channel: 'hotfix' },
|
|
740
|
+
];
|
|
741
|
+
// Process changes
|
|
742
|
+
changes.forEach(change => {
|
|
743
|
+
if (change.action === 'add') {
|
|
744
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, change.key, `Value for ${change.key}`, change.category, change.channel);
|
|
745
|
+
}
|
|
746
|
+
else if (change.action === 'modify') {
|
|
747
|
+
// Create before base time
|
|
748
|
+
const id = (0, uuid_1.v4)();
|
|
749
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, channel, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(id, testSessionId, change.key, `Original value for ${change.key}`, change.category, change.channel, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
|
|
750
|
+
// Update after base time
|
|
751
|
+
db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(`Modified value for ${change.key}`, id);
|
|
752
|
+
}
|
|
753
|
+
});
|
|
754
|
+
// Query different combinations
|
|
755
|
+
// 1. All added in 'main' channel
|
|
756
|
+
const mainAdded = db
|
|
757
|
+
.prepare(`
|
|
758
|
+
SELECT * FROM context_items
|
|
759
|
+
WHERE session_id = ?
|
|
760
|
+
AND created_at > ?
|
|
761
|
+
AND channel = ?
|
|
762
|
+
`)
|
|
763
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'main');
|
|
764
|
+
expect(mainAdded).toHaveLength(1);
|
|
765
|
+
expect(mainAdded[0].key).toBe('task_new_1');
|
|
766
|
+
// 2. All modified tasks
|
|
767
|
+
const modifiedTasks = db
|
|
768
|
+
.prepare(`
|
|
769
|
+
SELECT * FROM context_items
|
|
770
|
+
WHERE session_id = ?
|
|
771
|
+
AND created_at <= ?
|
|
772
|
+
AND updated_at > ?
|
|
773
|
+
AND category = ?
|
|
774
|
+
`)
|
|
775
|
+
.all(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), 'task');
|
|
776
|
+
expect(modifiedTasks).toHaveLength(1);
|
|
777
|
+
expect(modifiedTasks[0].key).toBe('task_mod_1');
|
|
778
|
+
// 3. All changes (added + modified) summary
|
|
779
|
+
const allAdded = db
|
|
780
|
+
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND created_at > ?')
|
|
781
|
+
.get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
782
|
+
const allModified = db
|
|
783
|
+
.prepare(`
|
|
784
|
+
SELECT COUNT(*) as count FROM context_items
|
|
785
|
+
WHERE session_id = ?
|
|
786
|
+
AND created_at <= ?
|
|
787
|
+
AND updated_at > ?
|
|
788
|
+
`)
|
|
789
|
+
.get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
790
|
+
expect(allAdded.count).toBe(2);
|
|
791
|
+
expect(allModified.count).toBe(2);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
describe('Summary Generation', () => {
|
|
795
|
+
it('should generate accurate diff summary', () => {
|
|
796
|
+
const baseTime = new Date();
|
|
797
|
+
baseTime.setHours(baseTime.getHours() - 1);
|
|
798
|
+
// Create scenario: 5 added, 3 modified, 2 deleted
|
|
799
|
+
// Add old items
|
|
800
|
+
const oldItemIds = [];
|
|
801
|
+
for (let i = 0; i < 5; i++) {
|
|
802
|
+
const id = (0, uuid_1.v4)();
|
|
803
|
+
oldItemIds.push(id);
|
|
804
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, testSessionId, `old_item_${i}`, `Original value ${i}`, (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()), (0, timestamps_1.toSQLiteTimestamp)(new Date(baseTime.getTime() - 60000).toISOString()));
|
|
805
|
+
}
|
|
806
|
+
// Create checkpoint for deletion tracking
|
|
807
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
808
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, testSessionId, 'summary-test', (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
809
|
+
oldItemIds.forEach(id => {
|
|
810
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, id);
|
|
811
|
+
});
|
|
812
|
+
// Count checkpoint items before any modifications
|
|
813
|
+
const originalCheckpointCount = db
|
|
814
|
+
.prepare('SELECT COUNT(*) as count FROM checkpoint_items WHERE checkpoint_id = ?')
|
|
815
|
+
.get(checkpointId);
|
|
816
|
+
// Modify 3 items
|
|
817
|
+
for (let i = 0; i < 3; i++) {
|
|
818
|
+
db.prepare('UPDATE context_items SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE session_id = ? AND key = ?').run(`Modified value ${i}`, testSessionId, `old_item_${i}`);
|
|
819
|
+
}
|
|
820
|
+
// Delete 2 items
|
|
821
|
+
db.prepare('DELETE FROM context_items WHERE session_id = ? AND key IN (?, ?)').run(testSessionId, 'old_item_3', 'old_item_4');
|
|
822
|
+
// Add 5 new items
|
|
823
|
+
for (let i = 0; i < 5; i++) {
|
|
824
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `new_item_${i}`, `New value ${i}`);
|
|
825
|
+
}
|
|
826
|
+
// Calculate summary
|
|
827
|
+
const added = db
|
|
828
|
+
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ? AND created_at > ?')
|
|
829
|
+
.get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
830
|
+
const modified = db
|
|
831
|
+
.prepare(`
|
|
832
|
+
SELECT COUNT(*) as count FROM context_items
|
|
833
|
+
WHERE session_id = ?
|
|
834
|
+
AND created_at <= ?
|
|
835
|
+
AND updated_at > ?
|
|
836
|
+
`)
|
|
837
|
+
.get(testSessionId, (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()), (0, timestamps_1.toSQLiteTimestamp)(baseTime.toISOString()));
|
|
838
|
+
// Get deleted count from checkpoint comparison
|
|
839
|
+
const currentItemCount = db
|
|
840
|
+
.prepare('SELECT COUNT(*) as count FROM context_items WHERE session_id = ?')
|
|
841
|
+
.get(testSessionId);
|
|
842
|
+
// deletedCount = original items - (current items - newly added items)
|
|
843
|
+
const deletedCount = originalCheckpointCount.count - (currentItemCount.count - added.count);
|
|
844
|
+
expect(added.count).toBe(5);
|
|
845
|
+
expect(modified.count).toBe(3);
|
|
846
|
+
expect(deletedCount).toBe(2);
|
|
847
|
+
// Summary string
|
|
848
|
+
const summary = `${added.count} added, ${modified.count} modified, ${deletedCount} deleted`;
|
|
849
|
+
expect(summary).toBe('5 added, 3 modified, 2 deleted');
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
});
|