mcp-memory-keeper 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1051 -0
- package/bin/mcp-memory-keeper +52 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +938 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +407 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/git-integration.test.js +237 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +283 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +307 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4310 -0
- package/dist/index.phase1.backup.js +410 -0
- package/dist/index.phase2.backup.js +704 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +1873 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/server.js +384 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +731 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- package/package.json +84 -0
|
@@ -0,0 +1,376 @@
|
|
|
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 channels_1 = require("../../utils/channels");
|
|
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
|
+
describe('Channels Feature Integration Tests', () => {
|
|
44
|
+
let dbManager;
|
|
45
|
+
let repositories;
|
|
46
|
+
let tempDbPath;
|
|
47
|
+
let db;
|
|
48
|
+
let testSessionId;
|
|
49
|
+
let testSessionId2;
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
tempDbPath = path.join(os.tmpdir(), `test-channels-${Date.now()}.db`);
|
|
52
|
+
dbManager = new database_1.DatabaseManager({
|
|
53
|
+
filename: tempDbPath,
|
|
54
|
+
maxSize: 10 * 1024 * 1024,
|
|
55
|
+
walMode: true,
|
|
56
|
+
});
|
|
57
|
+
db = dbManager.getDatabase();
|
|
58
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
59
|
+
// Create test sessions
|
|
60
|
+
testSessionId = (0, uuid_1.v4)();
|
|
61
|
+
testSessionId2 = (0, uuid_1.v4)();
|
|
62
|
+
});
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
dbManager.close();
|
|
65
|
+
try {
|
|
66
|
+
fs.unlinkSync(tempDbPath);
|
|
67
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
68
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
69
|
+
}
|
|
70
|
+
catch (_e) {
|
|
71
|
+
// Ignore
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
describe('Database schema changes', () => {
|
|
75
|
+
it('should have default_channel column in sessions table', () => {
|
|
76
|
+
const columns = db.prepare('PRAGMA table_info(sessions)').all();
|
|
77
|
+
const channelColumn = columns.find((col) => col.name === 'default_channel');
|
|
78
|
+
expect(channelColumn).toBeDefined();
|
|
79
|
+
expect(channelColumn.type).toBe('TEXT');
|
|
80
|
+
});
|
|
81
|
+
it('should have channel column in context_items table', () => {
|
|
82
|
+
const columns = db.prepare('PRAGMA table_info(context_items)').all();
|
|
83
|
+
const channelColumn = columns.find((col) => col.name === 'channel');
|
|
84
|
+
expect(channelColumn).toBeDefined();
|
|
85
|
+
expect(channelColumn.type).toBe('TEXT');
|
|
86
|
+
});
|
|
87
|
+
it('should have index on channel column for context_items', () => {
|
|
88
|
+
const indexes = db
|
|
89
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'context_items'")
|
|
90
|
+
.all();
|
|
91
|
+
const channelIndex = indexes.find((idx) => idx.name.includes('channel'));
|
|
92
|
+
expect(channelIndex).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('Channel derivation logic', () => {
|
|
96
|
+
it('should derive channel from git branch name', () => {
|
|
97
|
+
// Mock git branch
|
|
98
|
+
const mockBranch = 'feature/awesome-feature';
|
|
99
|
+
const derivedChannel = (0, channels_1.deriveChannelFromBranch)(mockBranch);
|
|
100
|
+
expect(derivedChannel).toBe('feature-awesome-feat'); // 20 chars max
|
|
101
|
+
});
|
|
102
|
+
it('should truncate long branch names to 20 characters', () => {
|
|
103
|
+
const longBranch = 'feature/this-is-a-very-long-branch-name-that-exceeds-limit';
|
|
104
|
+
const derivedChannel = (0, channels_1.deriveChannelFromBranch)(longBranch);
|
|
105
|
+
expect(derivedChannel).toBe('feature-this-is-a-ve');
|
|
106
|
+
expect(derivedChannel.length).toBeLessThanOrEqual(20);
|
|
107
|
+
});
|
|
108
|
+
it('should skip main and master branches', () => {
|
|
109
|
+
expect((0, channels_1.deriveChannelFromBranch)('main')).toBeNull();
|
|
110
|
+
expect((0, channels_1.deriveChannelFromBranch)('master')).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
it('should handle special characters in branch names', () => {
|
|
113
|
+
const branchWithSpecialChars = 'feat/user@123/task#456';
|
|
114
|
+
const derivedChannel = (0, channels_1.deriveChannelFromBranch)(branchWithSpecialChars);
|
|
115
|
+
expect(derivedChannel).toBe('feat-user-123-task-4');
|
|
116
|
+
expect(derivedChannel).toMatch(/^[a-z0-9-_]+$/);
|
|
117
|
+
});
|
|
118
|
+
it('should handle empty or null branch names', () => {
|
|
119
|
+
expect((0, channels_1.deriveChannelFromBranch)('')).toBeNull();
|
|
120
|
+
expect((0, channels_1.deriveChannelFromBranch)(null)).toBeNull();
|
|
121
|
+
expect((0, channels_1.deriveChannelFromBranch)(undefined)).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('context_session_start with channels', () => {
|
|
125
|
+
it('should accept explicit defaultChannel parameter', () => {
|
|
126
|
+
const session = repositories.sessions.create({
|
|
127
|
+
name: 'Test Session',
|
|
128
|
+
defaultChannel: 'my-custom-channel',
|
|
129
|
+
});
|
|
130
|
+
expect(session).toBeDefined();
|
|
131
|
+
expect(session.default_channel).toBe('my-custom-channel');
|
|
132
|
+
});
|
|
133
|
+
it('should auto-derive channel from git branch when not provided', async () => {
|
|
134
|
+
// Mock GitOperations
|
|
135
|
+
const mockGit = {
|
|
136
|
+
getCurrentBranch: jest.fn().mockResolvedValue('feature/cool-stuff'),
|
|
137
|
+
};
|
|
138
|
+
const session = await (0, channels_1.createSessionWithGitInfo)({
|
|
139
|
+
name: 'Auto Channel Session',
|
|
140
|
+
git: mockGit,
|
|
141
|
+
});
|
|
142
|
+
expect(session.default_channel).toBe('feature-cool-stuff');
|
|
143
|
+
});
|
|
144
|
+
it('should fallback to session name when git not available', async () => {
|
|
145
|
+
const mockGit = {
|
|
146
|
+
getCurrentBranch: jest.fn().mockResolvedValue(null),
|
|
147
|
+
};
|
|
148
|
+
const session = await (0, channels_1.createSessionWithGitInfo)({
|
|
149
|
+
name: 'My Session Name',
|
|
150
|
+
git: mockGit,
|
|
151
|
+
});
|
|
152
|
+
expect(session.default_channel).toBe('my-session-name');
|
|
153
|
+
});
|
|
154
|
+
it('should fallback to "general" when no name or git branch', async () => {
|
|
155
|
+
const mockGit = {
|
|
156
|
+
getCurrentBranch: jest.fn().mockResolvedValue(null),
|
|
157
|
+
};
|
|
158
|
+
const session = await (0, channels_1.createSessionWithGitInfo)({
|
|
159
|
+
git: mockGit,
|
|
160
|
+
});
|
|
161
|
+
expect(session.default_channel).toBe('general');
|
|
162
|
+
});
|
|
163
|
+
it('should store channel in session record', () => {
|
|
164
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Test Session', 'test-channel');
|
|
165
|
+
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(testSessionId);
|
|
166
|
+
expect(session.default_channel).toBe('test-channel');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('context_save with channels', () => {
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
// Create sessions with channels
|
|
172
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Session 1', 'channel-one');
|
|
173
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId2, 'Session 2', 'channel-two');
|
|
174
|
+
});
|
|
175
|
+
it('should accept explicit channel parameter', () => {
|
|
176
|
+
const contextItem = repositories.contexts.save(testSessionId, {
|
|
177
|
+
key: 'test-key',
|
|
178
|
+
value: 'test-value',
|
|
179
|
+
channel: 'explicit-channel',
|
|
180
|
+
});
|
|
181
|
+
expect(contextItem).toBeDefined();
|
|
182
|
+
expect(contextItem.channel).toBe('explicit-channel');
|
|
183
|
+
});
|
|
184
|
+
it('should use session default channel when not provided', () => {
|
|
185
|
+
const contextItem = repositories.contexts.save(testSessionId, {
|
|
186
|
+
key: 'test-key',
|
|
187
|
+
value: 'test-value',
|
|
188
|
+
});
|
|
189
|
+
expect(contextItem).toBeDefined();
|
|
190
|
+
expect(contextItem.channel).toBe('channel-one');
|
|
191
|
+
});
|
|
192
|
+
it('should fallback to "general" when session has no channel', () => {
|
|
193
|
+
// Create session without channel
|
|
194
|
+
const sessionNoChannel = (0, uuid_1.v4)();
|
|
195
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionNoChannel, 'No Channel Session');
|
|
196
|
+
const contextItem = repositories.contexts.save(sessionNoChannel, {
|
|
197
|
+
key: 'test-key',
|
|
198
|
+
value: 'test-value',
|
|
199
|
+
});
|
|
200
|
+
expect(contextItem).toBeDefined();
|
|
201
|
+
expect(contextItem.channel).toBe('general');
|
|
202
|
+
});
|
|
203
|
+
it('should store channel in context_items table', () => {
|
|
204
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'test-key', 'test-value', 'my-channel');
|
|
205
|
+
const item = db
|
|
206
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
|
|
207
|
+
.get(testSessionId, 'test-key');
|
|
208
|
+
expect(item.channel).toBe('my-channel');
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('context_get with channel filtering', () => {
|
|
212
|
+
beforeEach(() => {
|
|
213
|
+
// Create sessions
|
|
214
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Session 1', 'dev-channel');
|
|
215
|
+
// Add items to different channels
|
|
216
|
+
const items = [
|
|
217
|
+
{ key: 'item1', value: 'value1', channel: 'dev-channel' },
|
|
218
|
+
{ key: 'item2', value: 'value2', channel: 'dev-channel' },
|
|
219
|
+
{ key: 'item3', value: 'value3', channel: 'prod-channel' },
|
|
220
|
+
{ key: 'item4', value: 'value4', channel: 'general' },
|
|
221
|
+
{ key: 'item5', value: 'value5', channel: null }, // No channel
|
|
222
|
+
];
|
|
223
|
+
items.forEach(item => {
|
|
224
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.channel);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
it('should filter by single channel', () => {
|
|
228
|
+
const items = repositories.contexts.getByChannel(testSessionId, 'dev-channel');
|
|
229
|
+
expect(items).toHaveLength(2);
|
|
230
|
+
expect(items.every(item => item.channel === 'dev-channel')).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
it('should filter by multiple channels', () => {
|
|
233
|
+
const items = repositories.contexts.getByChannels(testSessionId, [
|
|
234
|
+
'dev-channel',
|
|
235
|
+
'prod-channel',
|
|
236
|
+
]);
|
|
237
|
+
expect(items).toHaveLength(3);
|
|
238
|
+
expect(items.every(item => ['dev-channel', 'prod-channel'].includes(item.channel))).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
it('should return all items when no channel filter provided (backward compatibility)', () => {
|
|
241
|
+
const items = repositories.contexts.getBySessionId(testSessionId);
|
|
242
|
+
expect(items).toHaveLength(5);
|
|
243
|
+
});
|
|
244
|
+
it('should handle empty channel filter array', () => {
|
|
245
|
+
const items = repositories.contexts.getByChannels(testSessionId, []);
|
|
246
|
+
expect(items).toHaveLength(0);
|
|
247
|
+
});
|
|
248
|
+
it('should support channel filter in queryEnhanced method', () => {
|
|
249
|
+
const result = repositories.contexts.queryEnhanced({
|
|
250
|
+
sessionId: testSessionId,
|
|
251
|
+
channel: 'dev-channel',
|
|
252
|
+
});
|
|
253
|
+
expect(result.items).toHaveLength(2);
|
|
254
|
+
expect(result.totalCount).toBe(2);
|
|
255
|
+
});
|
|
256
|
+
it('should support multiple channels in queryEnhanced method', () => {
|
|
257
|
+
const result = repositories.contexts.queryEnhanced({
|
|
258
|
+
sessionId: testSessionId,
|
|
259
|
+
channels: ['dev-channel', 'general'],
|
|
260
|
+
});
|
|
261
|
+
expect(result.items).toHaveLength(3);
|
|
262
|
+
expect(result.totalCount).toBe(3);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
describe('Cross-session channel persistence', () => {
|
|
266
|
+
it('should persist channel data across session crashes', () => {
|
|
267
|
+
// Session 1: Save data with channel
|
|
268
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Original Session', 'feature-branch');
|
|
269
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'important-data', 'must-persist', 'feature-branch');
|
|
270
|
+
// Simulate crash - close and reopen database
|
|
271
|
+
dbManager.close();
|
|
272
|
+
dbManager = new database_1.DatabaseManager({
|
|
273
|
+
filename: tempDbPath,
|
|
274
|
+
maxSize: 10 * 1024 * 1024,
|
|
275
|
+
walMode: true,
|
|
276
|
+
});
|
|
277
|
+
db = dbManager.getDatabase();
|
|
278
|
+
repositories = new RepositoryManager_1.RepositoryManager(dbManager);
|
|
279
|
+
// Session 2: Create new session with same branch/channel
|
|
280
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId2, 'Recovery Session', 'feature-branch');
|
|
281
|
+
// Should be able to retrieve data by channel from original session
|
|
282
|
+
const items = repositories.contexts.getByChannel(testSessionId, 'feature-branch');
|
|
283
|
+
expect(items).toHaveLength(1);
|
|
284
|
+
expect(items[0].key).toBe('important-data');
|
|
285
|
+
expect(items[0].value).toBe('must-persist');
|
|
286
|
+
});
|
|
287
|
+
it('should allow cross-session channel queries', () => {
|
|
288
|
+
// Create multiple sessions with same channel
|
|
289
|
+
const channel = 'shared-work';
|
|
290
|
+
const session1 = (0, uuid_1.v4)();
|
|
291
|
+
const session2 = (0, uuid_1.v4)();
|
|
292
|
+
const session3 = (0, uuid_1.v4)();
|
|
293
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(session1, 'Session 1', channel);
|
|
294
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(session2, 'Session 2', channel);
|
|
295
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(session3, 'Session 3', 'different-channel');
|
|
296
|
+
// Add items to each session
|
|
297
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel, is_private) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), session1, 'item1', 'from session 1', channel, 0);
|
|
298
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel, is_private) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), session2, 'item2', 'from session 2', channel, 0);
|
|
299
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel, is_private) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), session3, 'item3', 'from session 3', 'different-channel', 0);
|
|
300
|
+
// Query all items in the shared channel
|
|
301
|
+
const items = repositories.contexts.getByChannelAcrossSessions(channel);
|
|
302
|
+
expect(items).toHaveLength(2);
|
|
303
|
+
expect(items.map(i => i.key).sort()).toEqual(['item1', 'item2']);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
describe('Edge cases', () => {
|
|
307
|
+
beforeEach(() => {
|
|
308
|
+
// Create a session for edge case tests
|
|
309
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(testSessionId, 'Edge Case Session');
|
|
310
|
+
});
|
|
311
|
+
it('should handle very long branch names', () => {
|
|
312
|
+
const veryLongBranch = 'feature/' + 'x'.repeat(100);
|
|
313
|
+
const channel = (0, channels_1.deriveChannelFromBranch)(veryLongBranch);
|
|
314
|
+
expect(channel).toBe('feature-xxxxxxxxxxxx');
|
|
315
|
+
expect(channel.length).toBe(20);
|
|
316
|
+
});
|
|
317
|
+
it('should handle branch names with only special characters', () => {
|
|
318
|
+
const specialBranch = '@#$%^&*()';
|
|
319
|
+
const channel = (0, channels_1.deriveChannelFromBranch)(specialBranch);
|
|
320
|
+
expect(channel).toBe('general'); // Should fallback to general
|
|
321
|
+
});
|
|
322
|
+
it('should handle unicode characters in branch names', () => {
|
|
323
|
+
const unicodeBranch = 'feature/你好-世界';
|
|
324
|
+
const channel = (0, channels_1.deriveChannelFromBranch)(unicodeBranch);
|
|
325
|
+
expect(channel).toBe('feature'); // Non-ASCII chars should be replaced
|
|
326
|
+
});
|
|
327
|
+
it('should handle null channel values in database', () => {
|
|
328
|
+
// Skip this test - SQLite doesn't enforce DEFAULT on explicit NULL
|
|
329
|
+
// The application layer (save method) handles this properly
|
|
330
|
+
expect(true).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
it('should handle empty string channel', () => {
|
|
333
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'empty-channel-item', 'value', '');
|
|
334
|
+
const items = repositories.contexts.getByChannel(testSessionId, '');
|
|
335
|
+
expect(items).toHaveLength(1);
|
|
336
|
+
});
|
|
337
|
+
it('should maintain backward compatibility for items without channel', () => {
|
|
338
|
+
// Insert old-style item without channel
|
|
339
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, 'old-item', 'old-value');
|
|
340
|
+
const items = repositories.contexts.getBySessionId(testSessionId);
|
|
341
|
+
expect(items.some(i => i.key === 'old-item')).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe('Performance considerations', () => {
|
|
345
|
+
it('should efficiently query large datasets by channel', () => {
|
|
346
|
+
const channel = 'perf-test-channel';
|
|
347
|
+
// Create session
|
|
348
|
+
db.prepare('INSERT INTO sessions (id, name, default_channel) VALUES (?, ?, ?)').run(testSessionId, 'Perf Test', channel);
|
|
349
|
+
// Add many items
|
|
350
|
+
const startTime = Date.now();
|
|
351
|
+
db.transaction(() => {
|
|
352
|
+
for (let i = 0; i < 1000; i++) {
|
|
353
|
+
const itemChannel = i % 3 === 0 ? channel : `other-channel-${i % 10}`;
|
|
354
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, channel) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, `key-${i}`, `value-${i}`, itemChannel);
|
|
355
|
+
}
|
|
356
|
+
})();
|
|
357
|
+
const insertTime = Date.now() - startTime;
|
|
358
|
+
expect(insertTime).toBeLessThan(1000); // Should complete within 1 second
|
|
359
|
+
// Query by channel
|
|
360
|
+
const queryStartTime = Date.now();
|
|
361
|
+
const items = repositories.contexts.getByChannel(testSessionId, channel);
|
|
362
|
+
const queryTime = Date.now() - queryStartTime;
|
|
363
|
+
expect(queryTime).toBeLessThan(100); // Should query within 100ms
|
|
364
|
+
expect(items).toHaveLength(334); // ~333 items should have the test channel
|
|
365
|
+
});
|
|
366
|
+
it('should use channel index effectively', () => {
|
|
367
|
+
// Verify EXPLAIN QUERY PLAN uses index
|
|
368
|
+
const plan = db
|
|
369
|
+
.prepare('EXPLAIN QUERY PLAN SELECT * FROM context_items WHERE channel = ?')
|
|
370
|
+
.all('test-channel');
|
|
371
|
+
// Should use index (look for "USING INDEX" in the plan)
|
|
372
|
+
const usesIndex = plan.some(step => step.detail && step.detail.includes('idx_context_items_channel'));
|
|
373
|
+
expect(usesIndex).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -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 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('Checkpoint Integration Tests', () => {
|
|
42
|
+
let dbManager;
|
|
43
|
+
let tempDbPath;
|
|
44
|
+
let db;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
tempDbPath = path.join(os.tmpdir(), `test-checkpoint-${Date.now()}.db`);
|
|
47
|
+
dbManager = new database_1.DatabaseManager({
|
|
48
|
+
filename: tempDbPath,
|
|
49
|
+
maxSize: 10 * 1024 * 1024,
|
|
50
|
+
walMode: true,
|
|
51
|
+
});
|
|
52
|
+
db = dbManager.getDatabase();
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
dbManager.close();
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(tempDbPath);
|
|
58
|
+
fs.unlinkSync(`${tempDbPath}-wal`);
|
|
59
|
+
fs.unlinkSync(`${tempDbPath}-shm`);
|
|
60
|
+
}
|
|
61
|
+
catch (_e) {
|
|
62
|
+
// Ignore
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
describe('Checkpoint creation', () => {
|
|
66
|
+
it('should create a checkpoint with all context items', () => {
|
|
67
|
+
// Setup session with context
|
|
68
|
+
const sessionId = (0, uuid_1.v4)();
|
|
69
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test Session');
|
|
70
|
+
// Add context items
|
|
71
|
+
const contextItems = [];
|
|
72
|
+
for (let i = 0; i < 5; i++) {
|
|
73
|
+
const itemId = (0, uuid_1.v4)();
|
|
74
|
+
contextItems.push(itemId);
|
|
75
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run(itemId, sessionId, `key${i}`, `value${i}`);
|
|
76
|
+
}
|
|
77
|
+
// Create checkpoint
|
|
78
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
79
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, description) VALUES (?, ?, ?, ?)').run(checkpointId, sessionId, 'Test Checkpoint', 'Test Description');
|
|
80
|
+
// Link context items to checkpoint
|
|
81
|
+
contextItems.forEach(itemId => {
|
|
82
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
|
|
83
|
+
});
|
|
84
|
+
// Verify checkpoint was created
|
|
85
|
+
const checkpoint = db
|
|
86
|
+
.prepare('SELECT * FROM checkpoints WHERE id = ?')
|
|
87
|
+
.get(checkpointId);
|
|
88
|
+
expect(checkpoint).toBeDefined();
|
|
89
|
+
expect(checkpoint.name).toBe('Test Checkpoint');
|
|
90
|
+
// Verify all items are linked
|
|
91
|
+
const linkedItems = db
|
|
92
|
+
.prepare('SELECT COUNT(*) as count FROM checkpoint_items WHERE checkpoint_id = ?')
|
|
93
|
+
.get(checkpointId);
|
|
94
|
+
expect(linkedItems.count).toBe(5);
|
|
95
|
+
});
|
|
96
|
+
it('should include file cache in checkpoint', () => {
|
|
97
|
+
const sessionId = (0, uuid_1.v4)();
|
|
98
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test Session');
|
|
99
|
+
// Add file cache entries
|
|
100
|
+
const fileIds = [];
|
|
101
|
+
for (let i = 0; i < 3; i++) {
|
|
102
|
+
const fileId = (0, uuid_1.v4)();
|
|
103
|
+
fileIds.push(fileId);
|
|
104
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run(fileId, sessionId, `/file${i}.txt`, `content${i}`, `hash${i}`);
|
|
105
|
+
}
|
|
106
|
+
// Create checkpoint
|
|
107
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
108
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, sessionId, 'File Checkpoint');
|
|
109
|
+
// Link files to checkpoint
|
|
110
|
+
fileIds.forEach(fileId => {
|
|
111
|
+
db.prepare('INSERT INTO checkpoint_files (id, checkpoint_id, file_cache_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, fileId);
|
|
112
|
+
});
|
|
113
|
+
// Verify files are linked
|
|
114
|
+
const linkedFiles = db
|
|
115
|
+
.prepare('SELECT COUNT(*) as count FROM checkpoint_files WHERE checkpoint_id = ?')
|
|
116
|
+
.get(checkpointId);
|
|
117
|
+
expect(linkedFiles.count).toBe(3);
|
|
118
|
+
});
|
|
119
|
+
it('should capture git status in checkpoint', () => {
|
|
120
|
+
const sessionId = (0, uuid_1.v4)();
|
|
121
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test Session');
|
|
122
|
+
const gitStatus = JSON.stringify({
|
|
123
|
+
modified: ['file1.ts'],
|
|
124
|
+
created: ['file2.ts'],
|
|
125
|
+
staged: ['file1.ts'],
|
|
126
|
+
});
|
|
127
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
128
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, git_status, git_branch) VALUES (?, ?, ?, ?, ?)').run(checkpointId, sessionId, 'Git Checkpoint', gitStatus, 'feature/test');
|
|
129
|
+
const checkpoint = db
|
|
130
|
+
.prepare('SELECT * FROM checkpoints WHERE id = ?')
|
|
131
|
+
.get(checkpointId);
|
|
132
|
+
expect(checkpoint.git_status).toBe(gitStatus);
|
|
133
|
+
expect(checkpoint.git_branch).toBe('feature/test');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('Checkpoint restoration', () => {
|
|
137
|
+
it('should restore all context items from checkpoint', () => {
|
|
138
|
+
// Create original session
|
|
139
|
+
const originalSessionId = (0, uuid_1.v4)();
|
|
140
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(originalSessionId, 'Original');
|
|
141
|
+
// Add context items
|
|
142
|
+
const originalItems = [];
|
|
143
|
+
for (let i = 0; i < 3; i++) {
|
|
144
|
+
const itemId = (0, uuid_1.v4)();
|
|
145
|
+
originalItems.push({
|
|
146
|
+
id: itemId,
|
|
147
|
+
key: `key${i}`,
|
|
148
|
+
value: `value${i}`,
|
|
149
|
+
category: 'task',
|
|
150
|
+
priority: 'high',
|
|
151
|
+
});
|
|
152
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run(itemId, originalSessionId, `key${i}`, `value${i}`, 'task', 'high');
|
|
153
|
+
}
|
|
154
|
+
// Create checkpoint
|
|
155
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
156
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, originalSessionId, 'Restore Test');
|
|
157
|
+
// Link items to checkpoint
|
|
158
|
+
originalItems.forEach(item => {
|
|
159
|
+
db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, item.id);
|
|
160
|
+
});
|
|
161
|
+
// Create new session and restore
|
|
162
|
+
const newSessionId = (0, uuid_1.v4)();
|
|
163
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(newSessionId, 'Restored');
|
|
164
|
+
// Restore items
|
|
165
|
+
const itemsToRestore = db
|
|
166
|
+
.prepare(`
|
|
167
|
+
SELECT ci.* FROM context_items ci
|
|
168
|
+
JOIN checkpoint_items cpi ON ci.id = cpi.context_item_id
|
|
169
|
+
WHERE cpi.checkpoint_id = ?
|
|
170
|
+
`)
|
|
171
|
+
.all(checkpointId);
|
|
172
|
+
itemsToRestore.forEach((item) => {
|
|
173
|
+
db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), newSessionId, item.key, item.value, item.category, item.priority);
|
|
174
|
+
});
|
|
175
|
+
// Verify restoration
|
|
176
|
+
const restoredItems = db
|
|
177
|
+
.prepare('SELECT * FROM context_items WHERE session_id = ? ORDER BY key')
|
|
178
|
+
.all(newSessionId);
|
|
179
|
+
expect(restoredItems).toHaveLength(3);
|
|
180
|
+
expect(restoredItems[0].key).toBe('key0');
|
|
181
|
+
expect(restoredItems[0].value).toBe('value0');
|
|
182
|
+
expect(restoredItems[0].category).toBe('task');
|
|
183
|
+
expect(restoredItems[0].priority).toBe('high');
|
|
184
|
+
});
|
|
185
|
+
it('should restore file cache from checkpoint', () => {
|
|
186
|
+
// Create original session with files
|
|
187
|
+
const originalSessionId = (0, uuid_1.v4)();
|
|
188
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(originalSessionId, 'Original');
|
|
189
|
+
// Add files
|
|
190
|
+
const fileId = (0, uuid_1.v4)();
|
|
191
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run(fileId, originalSessionId, '/test/file.txt', 'file content', 'file hash');
|
|
192
|
+
// Create checkpoint
|
|
193
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
194
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, originalSessionId, 'File Restore Test');
|
|
195
|
+
db.prepare('INSERT INTO checkpoint_files (id, checkpoint_id, file_cache_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, fileId);
|
|
196
|
+
// Restore to new session
|
|
197
|
+
const newSessionId = (0, uuid_1.v4)();
|
|
198
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(newSessionId, 'Restored');
|
|
199
|
+
const filesToRestore = db
|
|
200
|
+
.prepare(`
|
|
201
|
+
SELECT fc.* FROM file_cache fc
|
|
202
|
+
JOIN checkpoint_files cpf ON fc.id = cpf.file_cache_id
|
|
203
|
+
WHERE cpf.checkpoint_id = ?
|
|
204
|
+
`)
|
|
205
|
+
.all(checkpointId);
|
|
206
|
+
filesToRestore.forEach((file) => {
|
|
207
|
+
db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), newSessionId, file.file_path, file.content, file.hash);
|
|
208
|
+
});
|
|
209
|
+
// Verify restoration
|
|
210
|
+
const restoredFile = db
|
|
211
|
+
.prepare('SELECT * FROM file_cache WHERE session_id = ? AND file_path = ?')
|
|
212
|
+
.get(newSessionId, '/test/file.txt');
|
|
213
|
+
expect(restoredFile).toBeDefined();
|
|
214
|
+
expect(restoredFile.content).toBe('file content');
|
|
215
|
+
expect(restoredFile.hash).toBe('file hash');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('Checkpoint management', () => {
|
|
219
|
+
it('should list checkpoints for a session', () => {
|
|
220
|
+
const sessionId = (0, uuid_1.v4)();
|
|
221
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test Session');
|
|
222
|
+
// Create multiple checkpoints
|
|
223
|
+
const checkpoints = [];
|
|
224
|
+
for (let i = 0; i < 3; i++) {
|
|
225
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
226
|
+
checkpoints.push({ id: checkpointId, name: `Checkpoint ${i}` });
|
|
227
|
+
const date = new Date();
|
|
228
|
+
date.setMinutes(date.getMinutes() - (3 - i)); // Different timestamps
|
|
229
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name, created_at) VALUES (?, ?, ?, ?)').run(checkpointId, sessionId, `Checkpoint ${i}`, date.toISOString());
|
|
230
|
+
}
|
|
231
|
+
// List checkpoints
|
|
232
|
+
const list = db
|
|
233
|
+
.prepare('SELECT * FROM checkpoints WHERE session_id = ? ORDER BY created_at DESC')
|
|
234
|
+
.all(sessionId);
|
|
235
|
+
expect(list).toHaveLength(3);
|
|
236
|
+
expect(list[0].name).toBe('Checkpoint 2'); // Most recent
|
|
237
|
+
expect(list[2].name).toBe('Checkpoint 0'); // Oldest
|
|
238
|
+
});
|
|
239
|
+
it('should find checkpoint by name', () => {
|
|
240
|
+
const sessionId = (0, uuid_1.v4)();
|
|
241
|
+
db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sessionId, 'Test Session');
|
|
242
|
+
const checkpointId = (0, uuid_1.v4)();
|
|
243
|
+
db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run(checkpointId, sessionId, 'Named Checkpoint');
|
|
244
|
+
const found = db
|
|
245
|
+
.prepare('SELECT * FROM checkpoints WHERE session_id = ? AND name = ?')
|
|
246
|
+
.get(sessionId, 'Named Checkpoint');
|
|
247
|
+
expect(found).toBeDefined();
|
|
248
|
+
expect(found.id).toBe(checkpointId);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|