Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chronicle/node/test/sessionReindexer.spec.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { describe, expect, it, vi } from 'vitest';
7
import type { IChatDebugFileLoggerService, IDebugLogEntry } from '../../../../platform/chat/common/chatDebugFileLoggerService';
8
import type { ISessionStore, SessionRow, TurnRow, FileRow, RefRow } from '../../../../platform/chronicle/common/sessionStore';
9
import { CancellationTokenSource } from '../../../../util/vs/base/common/cancellation';
10
import { reindexSessions } from '../sessionReindexer';
11
12
// ── Helpers ──────────────────────────────────────────────────────────────────
13
14
function makeEntry(overrides: Partial<IDebugLogEntry>): IDebugLogEntry {
15
return {
16
ts: Date.now(),
17
dur: 0,
18
sid: 'session-1',
19
type: 'generic',
20
name: '',
21
spanId: 'span-1',
22
status: 'ok',
23
attrs: {},
24
...overrides,
25
};
26
}
27
28
interface MockSessionStore extends ISessionStore {
29
upsertedSessions: SessionRow[];
30
insertedTurns: TurnRow[];
31
insertedFiles: FileRow[];
32
insertedRefs: RefRow[];
33
existingSessions: Set<string>;
34
}
35
36
function createMockStore(): MockSessionStore {
37
const mock: MockSessionStore = {
38
_serviceBrand: undefined as any,
39
upsertedSessions: [] as SessionRow[],
40
insertedTurns: [] as TurnRow[],
41
insertedFiles: [] as FileRow[],
42
insertedRefs: [] as RefRow[],
43
existingSessions: new Set<string>(),
44
45
getPath: () => '/tmp/test.db',
46
upsertSession: (s: SessionRow) => mock.upsertedSessions.push(s),
47
insertTurn: (t: TurnRow) => mock.insertedTurns.push(t),
48
insertCheckpoint: () => { },
49
insertFile: (f: FileRow) => mock.insertedFiles.push(f),
50
insertRef: (r: RefRow) => mock.insertedRefs.push(r),
51
indexWorkspaceArtifact: () => { },
52
search: () => [],
53
getSession: (id: string) => mock.existingSessions.has(id) ? { id } as SessionRow : undefined,
54
getTurns: () => [],
55
getFiles: () => [],
56
getRefs: () => [],
57
getMaxTurnIndex: () => -1,
58
getStats: () => ({ sessions: 0, turns: 0, checkpoints: 0, files: 0, refs: 0 }),
59
executeReadOnly: () => [],
60
executeReadOnlyFallback: () => [],
61
runInTransaction: (fn: () => void) => fn(),
62
close: () => { },
63
};
64
return mock;
65
}
66
67
function createMockDebugLogService(
68
sessionIds: string[],
69
entriesMap: Map<string, IDebugLogEntry[]>,
70
): IChatDebugFileLoggerService {
71
return {
72
_serviceBrand: undefined as any,
73
listSessionIds: async () => sessionIds,
74
streamEntries: async (sessionId: string, onEntry: (entry: IDebugLogEntry) => void) => {
75
const entries = entriesMap.get(sessionId) ?? [];
76
for (const entry of entries) {
77
onEntry(entry);
78
}
79
},
80
// Stubs for unused methods
81
startSession: async () => { },
82
startChildSession: () => { },
83
registerSpanSession: () => { },
84
endSession: async () => { },
85
flush: async () => { },
86
getLogPath: () => undefined,
87
getSessionDir: () => undefined,
88
getActiveSessionIds: () => [],
89
isDebugLogUri: () => false,
90
getSessionDirForResource: () => undefined,
91
setModelSnapshot: () => { },
92
debugLogsDir: undefined,
93
onDidEmitEntry: undefined as any,
94
readEntries: async () => [],
95
readTailEntries: async () => [],
96
} as any;
97
}
98
99
// ── Tests ────────────────────────────────────────────────────────────────────
100
101
describe('reindexSessions', () => {
102
it('processes a session with user + assistant turns', async () => {
103
const store = createMockStore();
104
const entries = new Map<string, IDebugLogEntry[]>();
105
entries.set('session-1', [
106
makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-1', attrs: { cwd: '/workspace' } }),
107
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Fix the bug' } }),
108
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'I fixed the bug by changing X' }] }]) } }),
109
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Now add tests' } }),
110
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'Added tests for X' }] }]) } }),
111
]);
112
113
const debugLog = createMockDebugLogService(['session-1'], entries);
114
const cts = new CancellationTokenSource();
115
const progress = vi.fn();
116
117
const result = await reindexSessions(store, debugLog, progress, cts.token);
118
119
expect(result).toEqual({ processed: 1, skipped: 0, cancelled: false });
120
expect(store.upsertedSessions).toHaveLength(1);
121
expect(store.upsertedSessions[0].cwd).toBe('/workspace');
122
expect(store.insertedTurns).toHaveLength(2);
123
expect(store.insertedTurns[0].user_message).toBe('Fix the bug');
124
expect(store.insertedTurns[0].assistant_response).toBe('I fixed the bug by changing X');
125
expect(store.insertedTurns[1].user_message).toBe('Now add tests');
126
expect(store.insertedTurns[1].assistant_response).toBe('Added tests for X');
127
});
128
129
it('extracts file paths from tool_call events', async () => {
130
const store = createMockStore();
131
const entries = new Map<string, IDebugLogEntry[]>();
132
entries.set('session-1', [
133
makeEntry({ type: 'tool_call', name: 'read_file', sid: 'session-1', attrs: { args: JSON.stringify({ filePath: '/src/foo.ts', startLine: 1, endLine: 10 }) } }),
134
makeEntry({ type: 'tool_call', name: 'create_file', sid: 'session-1', attrs: { args: JSON.stringify({ filePath: '/src/bar.ts', content: '// new' }) } }),
135
]);
136
137
const debugLog = createMockDebugLogService(['session-1'], entries);
138
const cts = new CancellationTokenSource();
139
140
await reindexSessions(store, debugLog, vi.fn(), cts.token);
141
142
expect(store.insertedFiles).toHaveLength(2);
143
expect(store.insertedFiles[0].file_path).toBe('/src/foo.ts');
144
expect(store.insertedFiles[1].file_path).toBe('/src/bar.ts');
145
});
146
147
it('extracts refs from GitHub MCP tool calls', async () => {
148
const store = createMockStore();
149
const entries = new Map<string, IDebugLogEntry[]>();
150
entries.set('session-1', [
151
makeEntry({
152
type: 'tool_call',
153
name: 'mcp_github_pull_request_read',
154
sid: 'session-1',
155
attrs: { args: JSON.stringify({ owner: 'microsoft', repo: 'vscode', pullNumber: 42 }) },
156
}),
157
]);
158
159
const debugLog = createMockDebugLogService(['session-1'], entries);
160
const cts = new CancellationTokenSource();
161
162
await reindexSessions(store, debugLog, vi.fn(), cts.token);
163
164
expect(store.insertedRefs).toHaveLength(1);
165
expect(store.insertedRefs[0]).toEqual(expect.objectContaining({ ref_type: 'pr', ref_value: '42' }));
166
expect(store.upsertedSessions[0].repository).toBe('microsoft/vscode');
167
});
168
169
it('extracts refs from terminal tool calls', async () => {
170
const store = createMockStore();
171
const entries = new Map<string, IDebugLogEntry[]>();
172
entries.set('session-1', [
173
makeEntry({
174
type: 'tool_call',
175
name: 'run_in_terminal',
176
sid: 'session-1',
177
attrs: {
178
args: JSON.stringify({ command: 'gh pr create --title "Fix" --body "desc"' }),
179
result: 'https://github.com/microsoft/vscode/pull/123',
180
},
181
}),
182
]);
183
184
const debugLog = createMockDebugLogService(['session-1'], entries);
185
const cts = new CancellationTokenSource();
186
187
await reindexSessions(store, debugLog, vi.fn(), cts.token);
188
189
expect(store.insertedRefs).toHaveLength(1);
190
expect(store.insertedRefs[0]).toEqual(expect.objectContaining({ ref_type: 'pr', ref_value: '123' }));
191
});
192
193
it('skips already-indexed sessions unless force=true', async () => {
194
const store = createMockStore();
195
store.existingSessions.add('session-1');
196
const entries = new Map<string, IDebugLogEntry[]>();
197
entries.set('session-1', [
198
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),
199
]);
200
201
const debugLog = createMockDebugLogService(['session-1'], entries);
202
const cts = new CancellationTokenSource();
203
204
// Default: skip
205
const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);
206
expect(result).toEqual({ processed: 0, skipped: 1, cancelled: false });
207
expect(store.insertedTurns).toHaveLength(0);
208
209
// Force: process
210
const result2 = await reindexSessions(store, debugLog, vi.fn(), cts.token, true);
211
expect(result2.processed).toBe(1);
212
});
213
214
it('respects cancellation token', async () => {
215
const store = createMockStore();
216
const entries = new Map<string, IDebugLogEntry[]>();
217
entries.set('session-1', [makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-1' })]);
218
entries.set('session-2', [makeEntry({ type: 'session_start', name: 'session_start', sid: 'session-2' })]);
219
220
const debugLog = createMockDebugLogService(['session-1', 'session-2'], entries);
221
const cts = new CancellationTokenSource();
222
223
// Cancel immediately
224
cts.cancel();
225
226
const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);
227
expect(result.cancelled).toBe(true);
228
expect(result.processed).toBe(0);
229
});
230
231
it('skips corrupt sessions and continues', async () => {
232
const store = createMockStore();
233
const entries = new Map<string, IDebugLogEntry[]>();
234
entries.set('session-good', [
235
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-good', attrs: { content: 'hello' } }),
236
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-good', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'hi' }] }]) } }),
237
]);
238
239
// Create a debug log service where session-bad throws
240
const debugLog = createMockDebugLogService(['session-bad', 'session-good'], entries);
241
const originalStream = debugLog.streamEntries.bind(debugLog);
242
(debugLog as any).streamEntries = async (sessionId: string, onEntry: any) => {
243
if (sessionId === 'session-bad') {
244
throw new Error('corrupt file');
245
}
246
return originalStream(sessionId, onEntry);
247
};
248
249
const cts = new CancellationTokenSource();
250
const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);
251
252
expect(result.processed).toBe(1);
253
expect(result.skipped).toBe(1);
254
expect(store.insertedTurns).toHaveLength(1);
255
});
256
257
it('truncates long user messages and assistant responses', async () => {
258
const store = createMockStore();
259
const longUserMsg = 'a'.repeat(200);
260
const longAssistantMsg = 'b'.repeat(2000);
261
const entries = new Map<string, IDebugLogEntry[]>();
262
entries.set('session-1', [
263
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: longUserMsg } }),
264
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: longAssistantMsg }] }]) } }),
265
]);
266
267
const debugLog = createMockDebugLogService(['session-1'], entries);
268
const cts = new CancellationTokenSource();
269
270
await reindexSessions(store, debugLog, vi.fn(), cts.token);
271
272
expect(store.insertedTurns[0].user_message!.length).toBeLessThanOrEqual(100);
273
expect(store.insertedTurns[0].assistant_response!.length).toBeLessThanOrEqual(1000);
274
});
275
276
it('handles sessions with no session_start event', async () => {
277
const store = createMockStore();
278
const entries = new Map<string, IDebugLogEntry[]>();
279
entries.set('session-1', [
280
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),
281
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'hi' }] }]) } }),
282
]);
283
284
const debugLog = createMockDebugLogService(['session-1'], entries);
285
const cts = new CancellationTokenSource();
286
287
await reindexSessions(store, debugLog, vi.fn(), cts.token);
288
289
expect(store.upsertedSessions).toHaveLength(1);
290
expect(store.upsertedSessions[0].id).toBe('session-1');
291
expect(store.upsertedSessions[0].host_type).toBe('vscode');
292
});
293
294
it('handles trailing user message without assistant response', async () => {
295
const store = createMockStore();
296
const entries = new Map<string, IDebugLogEntry[]>();
297
entries.set('session-1', [
298
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'hello' } }),
299
]);
300
301
const debugLog = createMockDebugLogService(['session-1'], entries);
302
const cts = new CancellationTokenSource();
303
304
await reindexSessions(store, debugLog, vi.fn(), cts.token);
305
306
expect(store.insertedTurns).toHaveLength(1);
307
expect(store.insertedTurns[0].user_message).toBe('hello');
308
expect(store.insertedTurns[0].assistant_response).toBeUndefined();
309
});
310
311
it('reports progress for each session', async () => {
312
const store = createMockStore();
313
const entries = new Map<string, IDebugLogEntry[]>();
314
entries.set('s1', [makeEntry({ type: 'session_start', name: 'session_start', sid: 's1' })]);
315
entries.set('s2', [makeEntry({ type: 'session_start', name: 'session_start', sid: 's2' })]);
316
317
const debugLog = createMockDebugLogService(['s1', 's2'], entries);
318
const cts = new CancellationTokenSource();
319
const progress = vi.fn();
320
321
await reindexSessions(store, debugLog, progress, cts.token);
322
323
expect(progress).toHaveBeenCalledTimes(2);
324
});
325
326
it('sets summary from first user message', async () => {
327
const store = createMockStore();
328
const entries = new Map<string, IDebugLogEntry[]>();
329
entries.set('session-1', [
330
makeEntry({ type: 'user_message', name: 'user_message', sid: 'session-1', attrs: { content: 'Implement a login page' } }),
331
makeEntry({ type: 'agent_response', name: 'agent_response', sid: 'session-1', attrs: { response: JSON.stringify([{ role: 'assistant', parts: [{ type: 'text', content: 'Done' }] }]) } }),
332
]);
333
334
const debugLog = createMockDebugLogService(['session-1'], entries);
335
const cts = new CancellationTokenSource();
336
337
await reindexSessions(store, debugLog, vi.fn(), cts.token);
338
339
expect(store.upsertedSessions[0].summary).toBe('Implement a login page');
340
});
341
342
it('returns empty result for no sessions', async () => {
343
const store = createMockStore();
344
const debugLog = createMockDebugLogService([], new Map());
345
const cts = new CancellationTokenSource();
346
347
const result = await reindexSessions(store, debugLog, vi.fn(), cts.token);
348
349
expect(result).toEqual({ processed: 0, skipped: 0, cancelled: false });
350
});
351
});
352
353