Path: blob/main/src/vs/workbench/contrib/chat/test/common/chatDebugEvents.test.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { URI } from '../../../../../base/common/uri.js';7import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugModelTurnEvent, IChatDebugSubagentInvocationEvent, IChatDebugToolCallEvent, IChatDebugUserMessageEvent, IChatDebugAgentResponseEvent } from '../../common/chatDebugService.js';8import { debugEventMatchesText, filterDebugEvents, filterDebugEventsByText, parseTimeToken, stripTimestampTokens } from '../../common/chatDebugEvents.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';1011const sessionResource = URI.parse('vscode-chat-session://local/test');1213function makeGenericEvent(overrides: Partial<IChatDebugGenericEvent> = {}): IChatDebugGenericEvent {14return {15kind: 'generic',16sessionResource,17created: new Date('2026-03-10T12:00:00Z'),18name: 'test-event',19level: ChatDebugLogLevel.Info,20...overrides,21};22}2324function makeToolCallEvent(overrides: Partial<IChatDebugToolCallEvent> = {}): IChatDebugToolCallEvent {25return {26kind: 'toolCall',27sessionResource,28created: new Date('2026-03-10T12:01:00Z'),29toolName: 'readFile',30...overrides,31};32}3334function makeModelTurnEvent(overrides: Partial<IChatDebugModelTurnEvent> = {}): IChatDebugModelTurnEvent {35return {36kind: 'modelTurn',37sessionResource,38created: new Date('2026-03-10T12:02:00Z'),39model: 'gpt-4o',40requestName: 'chat-request',41...overrides,42};43}4445function makeSubagentEvent(overrides: Partial<IChatDebugSubagentInvocationEvent> = {}): IChatDebugSubagentInvocationEvent {46return {47kind: 'subagentInvocation',48sessionResource,49created: new Date('2026-03-10T12:03:00Z'),50agentName: 'explorer',51...overrides,52};53}5455function makeUserMessageEvent(overrides: Partial<IChatDebugUserMessageEvent> = {}): IChatDebugUserMessageEvent {56return {57kind: 'userMessage',58sessionResource,59created: new Date('2026-03-10T12:04:00Z'),60message: 'hello world',61sections: [],62...overrides,63};64}6566function makeAgentResponseEvent(overrides: Partial<IChatDebugAgentResponseEvent> = {}): IChatDebugAgentResponseEvent {67return {68kind: 'agentResponse',69sessionResource,70created: new Date('2026-03-10T12:05:00Z'),71message: 'Here is the answer',72sections: [],73...overrides,74};75}7677suite('chatDebugEvents', () => {7879ensureNoDisposablesAreLeakedInTestSuite();8081suite('debugEventMatchesText', () => {82test('matches event kind', () => {83assert.strictEqual(debugEventMatchesText(makeToolCallEvent(), 'toolcall'), true);84assert.strictEqual(debugEventMatchesText(makeToolCallEvent(), 'generic'), false);85});8687test('matches toolCall tool name', () => {88assert.strictEqual(debugEventMatchesText(makeToolCallEvent({ toolName: 'readFile' }), 'readfile'), true);89assert.strictEqual(debugEventMatchesText(makeToolCallEvent({ toolName: 'readFile' }), 'writefile'), false);90});9192test('matches toolCall input and output', () => {93const event = makeToolCallEvent({ input: 'path/to/file.ts', output: 'file contents' });94assert.strictEqual(debugEventMatchesText(event, 'path/to'), true);95assert.strictEqual(debugEventMatchesText(event, 'contents'), true);96assert.strictEqual(debugEventMatchesText(event, 'missing'), false);97});9899test('matches modelTurn model and requestName', () => {100assert.strictEqual(debugEventMatchesText(makeModelTurnEvent({ model: 'gpt-4o' }), 'gpt-4o'), true);101assert.strictEqual(debugEventMatchesText(makeModelTurnEvent({ requestName: 'chat-request' }), 'chat-request'), true);102});103104test('matches generic event name, details, and category', () => {105const event = makeGenericEvent({ name: 'discovery', details: 'loaded 5 files', category: 'instructions' });106assert.strictEqual(debugEventMatchesText(event, 'discovery'), true);107assert.strictEqual(debugEventMatchesText(event, 'loaded'), true);108assert.strictEqual(debugEventMatchesText(event, 'instructions'), true);109assert.strictEqual(debugEventMatchesText(event, 'missing'), false);110});111112test('matches subagentInvocation agent name and description', () => {113const event = makeSubagentEvent({ agentName: 'explorer', description: 'search codebase' });114assert.strictEqual(debugEventMatchesText(event, 'explorer'), true);115assert.strictEqual(debugEventMatchesText(event, 'codebase'), true);116});117118test('matches userMessage message and sections', () => {119const event = makeUserMessageEvent({120message: 'fix the bug',121sections: [{ name: 'system', content: 'you are a helpful assistant' }],122});123assert.strictEqual(debugEventMatchesText(event, 'fix'), true);124assert.strictEqual(debugEventMatchesText(event, 'system'), true);125assert.strictEqual(debugEventMatchesText(event, 'helpful'), true);126});127128test('matches agentResponse message and sections', () => {129const event = makeAgentResponseEvent({130message: 'done',131sections: [{ name: 'result', content: 'applied 3 edits' }],132});133assert.strictEqual(debugEventMatchesText(event, 'done'), true);134assert.strictEqual(debugEventMatchesText(event, 'result'), true);135assert.strictEqual(debugEventMatchesText(event, 'edits'), true);136});137});138139suite('parseTimeToken', () => {140test('parses year-only before token', () => {141const result = parseTimeToken('before:2026', 'before');142assert.strictEqual(result, new Date(2026, 11, 31, 23, 59, 59, 999).getTime());143});144145test('parses year-month before token', () => {146const result = parseTimeToken('before:2026-03', 'before');147// End of March 2026148assert.strictEqual(result, new Date(2026, 3, 0, 23, 59, 59, 999).getTime());149});150151test('parses full date before token', () => {152const result = parseTimeToken('before:2026-03-10', 'before');153assert.strictEqual(result, new Date(2026, 2, 10, 23, 59, 59, 999).getTime());154});155156test('parses year-only after token', () => {157const result = parseTimeToken('after:2026', 'after');158assert.strictEqual(result, new Date(2026, 0, 1, 0, 0, 0, 0).getTime());159});160161test('parses full date after token', () => {162const result = parseTimeToken('after:2026-03-10', 'after');163assert.strictEqual(result, new Date(2026, 2, 10, 0, 0, 0, 0).getTime());164});165166test('returns undefined when token is absent', () => {167assert.strictEqual(parseTimeToken('some text', 'before'), undefined);168assert.strictEqual(parseTimeToken('some text', 'after'), undefined);169});170});171172suite('stripTimestampTokens', () => {173test('strips before token', () => {174assert.strictEqual(stripTimestampTokens('before:2026-03 hello'), 'hello');175});176177test('strips after token', () => {178assert.strictEqual(stripTimestampTokens('after:2026-03-10 hello'), 'hello');179});180181test('strips both tokens', () => {182assert.strictEqual(stripTimestampTokens('after:2026-03 before:2026-04 hello'), 'hello');183});184185test('returns text unchanged when no tokens', () => {186assert.strictEqual(stripTimestampTokens('hello world'), 'hello world');187});188});189190suite('filterDebugEventsByText', () => {191// parseTimeToken uses local-time Date constructors, so event timestamps192// must also be in local time to produce predictable comparisons.193const events: readonly IChatDebugEvent[] = [194makeGenericEvent({ name: 'discovery', category: 'instructions', created: new Date(2026, 2, 10, 10, 0, 0) }),195makeToolCallEvent({ toolName: 'readFile', created: new Date(2026, 2, 10, 11, 0, 0) }),196makeToolCallEvent({ toolName: 'writeFile', created: new Date(2026, 2, 10, 12, 0, 0) }),197makeModelTurnEvent({ model: 'gpt-4o', created: new Date(2026, 2, 10, 13, 0, 0) }),198];199200test('filters by inclusion term', () => {201const result = filterDebugEventsByText(events, 'readfile');202assert.strictEqual(result.length, 1);203assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');204});205206test('filters by exclusion term', () => {207const result = filterDebugEventsByText(events, '!readfile');208assert.strictEqual(result.length, 3);209});210211test('handles comma-separated terms as OR', () => {212const result = filterDebugEventsByText(events, 'readfile, writefile');213assert.strictEqual(result.length, 2);214});215216test('combines inclusion and exclusion', () => {217const result = filterDebugEventsByText(events, 'toolcall, !readfile');218assert.strictEqual(result.length, 1);219assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');220});221222test('filters by before timestamp', () => {223const result = filterDebugEventsByText(events, 'before:2026-03-10t11');224assert.strictEqual(result.length, 2); // 10:00 and 11:00 (before rounds up to 11:59:59)225});226227test('filters by after timestamp', () => {228const result = filterDebugEventsByText(events, 'after:2026-03-10t12');229assert.strictEqual(result.length, 2); // 12:00 and 13:00230});231232test('combines timestamp and text filters', () => {233const result = filterDebugEventsByText(events, 'after:2026-03-10t11 toolcall');234assert.strictEqual(result.length, 2); // writeFile at 12:00 and readFile at 11:00235});236237test('returns all events with empty filter', () => {238const result = filterDebugEventsByText(events, '');239assert.strictEqual(result.length, 4);240});241});242243suite('filterDebugEvents', () => {244const events: readonly IChatDebugEvent[] = [245makeGenericEvent({ name: 'event-1', created: new Date('2026-03-10T10:00:00Z') }),246makeToolCallEvent({ toolName: 'readFile', created: new Date('2026-03-10T11:00:00Z') }),247makeToolCallEvent({ toolName: 'writeFile', created: new Date('2026-03-10T12:00:00Z') }),248makeModelTurnEvent({ model: 'gpt-4o', created: new Date('2026-03-10T13:00:00Z') }),249makeSubagentEvent({ agentName: 'explorer', created: new Date('2026-03-10T14:00:00Z') }),250];251252test('returns all events with empty options', () => {253assert.deepStrictEqual(filterDebugEvents(events, {}), events);254});255256test('filters by kind', () => {257const result = filterDebugEvents(events, { kind: 'toolCall' });258assert.strictEqual(result.length, 2);259assert.ok(result.every(e => e.kind === 'toolCall'));260});261262test('filters by kind with no matches', () => {263const result = filterDebugEvents(events, { kind: 'userMessage' });264assert.strictEqual(result.length, 0);265});266267test('filters by text', () => {268const result = filterDebugEvents(events, { filter: 'readfile' });269assert.strictEqual(result.length, 1);270assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');271});272273test('limits to N most recent', () => {274const result = filterDebugEvents(events, { limit: 2 });275assert.strictEqual(result.length, 2);276assert.strictEqual(result[0].kind, 'modelTurn');277assert.strictEqual(result[1].kind, 'subagentInvocation');278});279280test('limit larger than event count returns all', () => {281const result = filterDebugEvents(events, { limit: 100 });282assert.strictEqual(result.length, 5);283});284285test('limit of 0 returns all', () => {286const result = filterDebugEvents(events, { limit: 0 });287assert.strictEqual(result.length, 5);288});289290test('limit of negative returns all', () => {291const result = filterDebugEvents(events, { limit: -1 });292assert.strictEqual(result.length, 5);293});294295test('combines kind and text filters', () => {296const result = filterDebugEvents(events, { kind: 'toolCall', filter: 'readfile' });297assert.strictEqual(result.length, 1);298assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'readFile');299});300301test('combines kind and limit', () => {302const result = filterDebugEvents(events, { kind: 'toolCall', limit: 1 });303assert.strictEqual(result.length, 1);304assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');305});306307test('combines text filter and limit', () => {308const result = filterDebugEvents(events, { filter: 'toolcall', limit: 1 });309assert.strictEqual(result.length, 1);310assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'writeFile');311});312313test('combines all three filters', () => {314const allToolCalls: readonly IChatDebugEvent[] = [315makeToolCallEvent({ toolName: 'readFile', created: new Date('2026-03-10T10:00:00Z') }),316makeToolCallEvent({ toolName: 'writeFile', created: new Date('2026-03-10T11:00:00Z') }),317makeToolCallEvent({ toolName: 'listDir', created: new Date('2026-03-10T12:00:00Z') }),318makeGenericEvent({ name: 'unrelated', created: new Date('2026-03-10T13:00:00Z') }),319];320// kind=toolCall, exclude readFile, limit=1 → should get the most recent non-readFile toolCall (listDir)321const result = filterDebugEvents(allToolCalls, { kind: 'toolCall', filter: '!readfile', limit: 1 });322assert.strictEqual(result.length, 1);323assert.strictEqual((result[0] as IChatDebugToolCallEvent).toolName, 'listDir');324});325});326});327328329