Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatDebugEvents.test.ts
13406 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 assert from 'assert';
7
import { URI } from '../../../../../base/common/uri.js';
8
import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugModelTurnEvent, IChatDebugSubagentInvocationEvent, IChatDebugToolCallEvent, IChatDebugUserMessageEvent, IChatDebugAgentResponseEvent } from '../../common/chatDebugService.js';
9
import { debugEventMatchesText, filterDebugEvents, filterDebugEventsByText, parseTimeToken, stripTimestampTokens } from '../../common/chatDebugEvents.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
12
const sessionResource = URI.parse('vscode-chat-session://local/test');
13
14
function makeGenericEvent(overrides: Partial<IChatDebugGenericEvent> = {}): IChatDebugGenericEvent {
15
return {
16
kind: 'generic',
17
sessionResource,
18
created: new Date('2026-03-10T12:00:00Z'),
19
name: 'test-event',
20
level: ChatDebugLogLevel.Info,
21
...overrides,
22
};
23
}
24
25
function makeToolCallEvent(overrides: Partial<IChatDebugToolCallEvent> = {}): IChatDebugToolCallEvent {
26
return {
27
kind: 'toolCall',
28
sessionResource,
29
created: new Date('2026-03-10T12:01:00Z'),
30
toolName: 'readFile',
31
...overrides,
32
};
33
}
34
35
function makeModelTurnEvent(overrides: Partial<IChatDebugModelTurnEvent> = {}): IChatDebugModelTurnEvent {
36
return {
37
kind: 'modelTurn',
38
sessionResource,
39
created: new Date('2026-03-10T12:02:00Z'),
40
model: 'gpt-4o',
41
requestName: 'chat-request',
42
...overrides,
43
};
44
}
45
46
function makeSubagentEvent(overrides: Partial<IChatDebugSubagentInvocationEvent> = {}): IChatDebugSubagentInvocationEvent {
47
return {
48
kind: 'subagentInvocation',
49
sessionResource,
50
created: new Date('2026-03-10T12:03:00Z'),
51
agentName: 'explorer',
52
...overrides,
53
};
54
}
55
56
function makeUserMessageEvent(overrides: Partial<IChatDebugUserMessageEvent> = {}): IChatDebugUserMessageEvent {
57
return {
58
kind: 'userMessage',
59
sessionResource,
60
created: new Date('2026-03-10T12:04:00Z'),
61
message: 'hello world',
62
sections: [],
63
...overrides,
64
};
65
}
66
67
function makeAgentResponseEvent(overrides: Partial<IChatDebugAgentResponseEvent> = {}): IChatDebugAgentResponseEvent {
68
return {
69
kind: 'agentResponse',
70
sessionResource,
71
created: new Date('2026-03-10T12:05:00Z'),
72
message: 'Here is the answer',
73
sections: [],
74
...overrides,
75
};
76
}
77
78
suite('chatDebugEvents', () => {
79
80
ensureNoDisposablesAreLeakedInTestSuite();
81
82
suite('debugEventMatchesText', () => {
83
test('matches event kind', () => {
84
assert.strictEqual(debugEventMatchesText(makeToolCallEvent(), 'toolcall'), true);
85
assert.strictEqual(debugEventMatchesText(makeToolCallEvent(), 'generic'), false);
86
});
87
88
test('matches toolCall tool name', () => {
89
assert.strictEqual(debugEventMatchesText(makeToolCallEvent({ toolName: 'readFile' }), 'readfile'), true);
90
assert.strictEqual(debugEventMatchesText(makeToolCallEvent({ toolName: 'readFile' }), 'writefile'), false);
91
});
92
93
test('matches toolCall input and output', () => {
94
const event = makeToolCallEvent({ input: 'path/to/file.ts', output: 'file contents' });
95
assert.strictEqual(debugEventMatchesText(event, 'path/to'), true);
96
assert.strictEqual(debugEventMatchesText(event, 'contents'), true);
97
assert.strictEqual(debugEventMatchesText(event, 'missing'), false);
98
});
99
100
test('matches modelTurn model and requestName', () => {
101
assert.strictEqual(debugEventMatchesText(makeModelTurnEvent({ model: 'gpt-4o' }), 'gpt-4o'), true);
102
assert.strictEqual(debugEventMatchesText(makeModelTurnEvent({ requestName: 'chat-request' }), 'chat-request'), true);
103
});
104
105
test('matches generic event name, details, and category', () => {
106
const event = makeGenericEvent({ name: 'discovery', details: 'loaded 5 files', category: 'instructions' });
107
assert.strictEqual(debugEventMatchesText(event, 'discovery'), true);
108
assert.strictEqual(debugEventMatchesText(event, 'loaded'), true);
109
assert.strictEqual(debugEventMatchesText(event, 'instructions'), true);
110
assert.strictEqual(debugEventMatchesText(event, 'missing'), false);
111
});
112
113
test('matches subagentInvocation agent name and description', () => {
114
const event = makeSubagentEvent({ agentName: 'explorer', description: 'search codebase' });
115
assert.strictEqual(debugEventMatchesText(event, 'explorer'), true);
116
assert.strictEqual(debugEventMatchesText(event, 'codebase'), true);
117
});
118
119
test('matches userMessage message and sections', () => {
120
const event = makeUserMessageEvent({
121
message: 'fix the bug',
122
sections: [{ name: 'system', content: 'you are a helpful assistant' }],
123
});
124
assert.strictEqual(debugEventMatchesText(event, 'fix'), true);
125
assert.strictEqual(debugEventMatchesText(event, 'system'), true);
126
assert.strictEqual(debugEventMatchesText(event, 'helpful'), true);
127
});
128
129
test('matches agentResponse message and sections', () => {
130
const event = makeAgentResponseEvent({
131
message: 'done',
132
sections: [{ name: 'result', content: 'applied 3 edits' }],
133
});
134
assert.strictEqual(debugEventMatchesText(event, 'done'), true);
135
assert.strictEqual(debugEventMatchesText(event, 'result'), true);
136
assert.strictEqual(debugEventMatchesText(event, 'edits'), true);
137
});
138
});
139
140
suite('parseTimeToken', () => {
141
test('parses year-only before token', () => {
142
const result = parseTimeToken('before:2026', 'before');
143
assert.strictEqual(result, new Date(2026, 11, 31, 23, 59, 59, 999).getTime());
144
});
145
146
test('parses year-month before token', () => {
147
const result = parseTimeToken('before:2026-03', 'before');
148
// End of March 2026
149
assert.strictEqual(result, new Date(2026, 3, 0, 23, 59, 59, 999).getTime());
150
});
151
152
test('parses full date before token', () => {
153
const result = parseTimeToken('before:2026-03-10', 'before');
154
assert.strictEqual(result, new Date(2026, 2, 10, 23, 59, 59, 999).getTime());
155
});
156
157
test('parses year-only after token', () => {
158
const result = parseTimeToken('after:2026', 'after');
159
assert.strictEqual(result, new Date(2026, 0, 1, 0, 0, 0, 0).getTime());
160
});
161
162
test('parses full date after token', () => {
163
const result = parseTimeToken('after:2026-03-10', 'after');
164
assert.strictEqual(result, new Date(2026, 2, 10, 0, 0, 0, 0).getTime());
165
});
166
167
test('returns undefined when token is absent', () => {
168
assert.strictEqual(parseTimeToken('some text', 'before'), undefined);
169
assert.strictEqual(parseTimeToken('some text', 'after'), undefined);
170
});
171
});
172
173
suite('stripTimestampTokens', () => {
174
test('strips before token', () => {
175
assert.strictEqual(stripTimestampTokens('before:2026-03 hello'), 'hello');
176
});
177
178
test('strips after token', () => {
179
assert.strictEqual(stripTimestampTokens('after:2026-03-10 hello'), 'hello');
180
});
181
182
test('strips both tokens', () => {
183
assert.strictEqual(stripTimestampTokens('after:2026-03 before:2026-04 hello'), 'hello');
184
});
185
186
test('returns text unchanged when no tokens', () => {
187
assert.strictEqual(stripTimestampTokens('hello world'), 'hello world');
188
});
189
});
190
191
suite('filterDebugEventsByText', () => {
192
// parseTimeToken uses local-time Date constructors, so event timestamps
193
// must also be in local time to produce predictable comparisons.
194
const events: readonly IChatDebugEvent[] = [
195
makeGenericEvent({ name: 'discovery', category: 'instructions', created: new Date(2026, 2, 10, 10, 0, 0) }),
196
makeToolCallEvent({ toolName: 'readFile', created: new Date(2026, 2, 10, 11, 0, 0) }),
197
makeToolCallEvent({ toolName: 'writeFile', created: new Date(2026, 2, 10, 12, 0, 0) }),
198
makeModelTurnEvent({ model: 'gpt-4o', created: new Date(2026, 2, 10, 13, 0, 0) }),
199
];
200
201
test('filters by inclusion term', () => {
202
const result = filterDebugEventsByText(events, 'readfile');
203
assert.strictEqual(result.length, 1);
204
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');
205
});
206
207
test('filters by exclusion term', () => {
208
const result = filterDebugEventsByText(events, '!readfile');
209
assert.strictEqual(result.length, 3);
210
});
211
212
test('handles comma-separated terms as OR', () => {
213
const result = filterDebugEventsByText(events, 'readfile, writefile');
214
assert.strictEqual(result.length, 2);
215
});
216
217
test('combines inclusion and exclusion', () => {
218
const result = filterDebugEventsByText(events, 'toolcall, !readfile');
219
assert.strictEqual(result.length, 1);
220
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');
221
});
222
223
test('filters by before timestamp', () => {
224
const result = filterDebugEventsByText(events, 'before:2026-03-10t11');
225
assert.strictEqual(result.length, 2); // 10:00 and 11:00 (before rounds up to 11:59:59)
226
});
227
228
test('filters by after timestamp', () => {
229
const result = filterDebugEventsByText(events, 'after:2026-03-10t12');
230
assert.strictEqual(result.length, 2); // 12:00 and 13:00
231
});
232
233
test('combines timestamp and text filters', () => {
234
const result = filterDebugEventsByText(events, 'after:2026-03-10t11 toolcall');
235
assert.strictEqual(result.length, 2); // writeFile at 12:00 and readFile at 11:00
236
});
237
238
test('returns all events with empty filter', () => {
239
const result = filterDebugEventsByText(events, '');
240
assert.strictEqual(result.length, 4);
241
});
242
});
243
244
suite('filterDebugEvents', () => {
245
const events: readonly IChatDebugEvent[] = [
246
makeGenericEvent({ name: 'event-1', created: new Date('2026-03-10T10:00:00Z') }),
247
makeToolCallEvent({ toolName: 'readFile', created: new Date('2026-03-10T11:00:00Z') }),
248
makeToolCallEvent({ toolName: 'writeFile', created: new Date('2026-03-10T12:00:00Z') }),
249
makeModelTurnEvent({ model: 'gpt-4o', created: new Date('2026-03-10T13:00:00Z') }),
250
makeSubagentEvent({ agentName: 'explorer', created: new Date('2026-03-10T14:00:00Z') }),
251
];
252
253
test('returns all events with empty options', () => {
254
assert.deepStrictEqual(filterDebugEvents(events, {}), events);
255
});
256
257
test('filters by kind', () => {
258
const result = filterDebugEvents(events, { kind: 'toolCall' });
259
assert.strictEqual(result.length, 2);
260
assert.ok(result.every(e => e.kind === 'toolCall'));
261
});
262
263
test('filters by kind with no matches', () => {
264
const result = filterDebugEvents(events, { kind: 'userMessage' });
265
assert.strictEqual(result.length, 0);
266
});
267
268
test('filters by text', () => {
269
const result = filterDebugEvents(events, { filter: 'readfile' });
270
assert.strictEqual(result.length, 1);
271
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');
272
});
273
274
test('limits to N most recent', () => {
275
const result = filterDebugEvents(events, { limit: 2 });
276
assert.strictEqual(result.length, 2);
277
assert.strictEqual(result[0].kind, 'modelTurn');
278
assert.strictEqual(result[1].kind, 'subagentInvocation');
279
});
280
281
test('limit larger than event count returns all', () => {
282
const result = filterDebugEvents(events, { limit: 100 });
283
assert.strictEqual(result.length, 5);
284
});
285
286
test('limit of 0 returns all', () => {
287
const result = filterDebugEvents(events, { limit: 0 });
288
assert.strictEqual(result.length, 5);
289
});
290
291
test('limit of negative returns all', () => {
292
const result = filterDebugEvents(events, { limit: -1 });
293
assert.strictEqual(result.length, 5);
294
});
295
296
test('combines kind and text filters', () => {
297
const result = filterDebugEvents(events, { kind: 'toolCall', filter: 'readfile' });
298
assert.strictEqual(result.length, 1);
299
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');
300
});
301
302
test('combines kind and limit', () => {
303
const result = filterDebugEvents(events, { kind: 'toolCall', limit: 1 });
304
assert.strictEqual(result.length, 1);
305
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');
306
});
307
308
test('combines text filter and limit', () => {
309
const result = filterDebugEvents(events, { filter: 'toolcall', limit: 1 });
310
assert.strictEqual(result.length, 1);
311
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');
312
});
313
314
test('combines all three filters', () => {
315
const allToolCalls: readonly IChatDebugEvent[] = [
316
makeToolCallEvent({ toolName: 'readFile', created: new Date('2026-03-10T10:00:00Z') }),
317
makeToolCallEvent({ toolName: 'writeFile', created: new Date('2026-03-10T11:00:00Z') }),
318
makeToolCallEvent({ toolName: 'listDir', created: new Date('2026-03-10T12:00:00Z') }),
319
makeGenericEvent({ name: 'unrelated', created: new Date('2026-03-10T13:00:00Z') }),
320
];
321
// kind=toolCall, exclude readFile, limit=1 → should get the most recent non-readFile toolCall (listDir)
322
const result = filterDebugEvents(allToolCalls, { kind: 'toolCall', filter: '!readfile', limit: 1 });
323
assert.strictEqual(result.length, 1);
324
assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'listDir');
325
});
326
});
327
});
328
329