Path: blob/main/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.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 { Emitter } from '../../../../../base/common/event.js';7import { URI } from '../../../../../base/common/uri.js';8import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';9import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';10import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugService } from '../../common/chatDebugService.js';11import { ChatDebugServiceImpl } from '../../common/chatDebugServiceImpl.js';12import { LocalChatSessionUri } from '../../common/model/chatUri.js';13import { IChatAgentService, IChatAgentInvocationEvent } from '../../common/participants/chatAgents.js';14import { IChatService } from '../../common/chatService/chatService.js';15import { PromptsDebugContribution } from '../../browser/promptsDebugContribution.js';16import { ILocalPromptPath, IPromptDiscoveryInfo, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';17import { PromptsType } from '../../common/promptSyntax/promptTypes.js';1819function createLocalPromptPath(path: string, name: string): ILocalPromptPath {20return {21uri: URI.file(path),22name,23storage: PromptsStorage.local,24type: PromptsType.instructions,25};26}2728function isGenericEvent(event: IChatDebugEvent): event is IChatDebugGenericEvent {29return event.kind === 'generic';30}3132async function flushAsyncLogging(): Promise<void> {33await new Promise<void>(resolve => setTimeout(resolve, 0));34}3536suite('PromptsDebugContribution', () => {37const disposables = ensureNoDisposablesAreLeakedInTestSuite();3839let chatDebugService: ChatDebugServiceImpl;40let willInvokeAgentEmitter: Emitter<IChatAgentInvocationEvent>;41let instaService: TestInstantiationService;42let promptsService: Partial<IPromptsService>;43const emptyDiscoveryInfo = (type: PromptsType): IPromptDiscoveryInfo => ({ type, files: [], durationInMillis: 0 });4445setup(() => {46instaService = disposables.add(new TestInstantiationService());4748chatDebugService = disposables.add(new ChatDebugServiceImpl());49instaService.stub(IChatDebugService, chatDebugService);5051willInvokeAgentEmitter = disposables.add(new Emitter<IChatAgentInvocationEvent>());52instaService.stub(IChatAgentService, { onWillInvokeAgent: willInvokeAgentEmitter.event } as Partial<IChatAgentService>);53instaService.stub(IChatService, { onDidDisposeSession: disposables.add(new Emitter()).event } as Partial<IChatService>);54promptsService = {55getDiscoveryInfo: async type => emptyDiscoveryInfo(type),56};57instaService.stub(IPromptsService, promptsService);58});5960test('should forward discovery events to chat debug service', async () => {61disposables.add(instaService.createInstance(PromptsDebugContribution));6263const firedEvents: IChatDebugEvent[] = [];64disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));6566promptsService.getDiscoveryInfo = async type => ({67type,68durationInMillis: 7,69files: type === PromptsType.instructions ? [{70status: 'loaded' as const,71promptPath: createLocalPromptPath('/workspace/.github/instructions/test.instructions.md', 'test.instructions.md'),72}] : [],73});7475willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });76await flushAsyncLogging();7778assert.strictEqual(firedEvents.length, 5);79const event = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery');80assert.ok(event);81assert.strictEqual(event.kind, 'generic');82assert.ok(event.sessionResource);83assert.strictEqual(event.name, 'Instructions Discovery');84assert.ok(event.details?.includes('Resolved 1 instruction'));85assert.strictEqual(event.category, 'discovery');86});8788test('should store discoveryInfo and resolve via resolveEvent', async () => {89disposables.add(instaService.createInstance(PromptsDebugContribution));9091const firedEvents: IChatDebugEvent[] = [];92disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));9394const discoveryInfo: IPromptDiscoveryInfo = {95type: PromptsType.instructions,96durationInMillis: 11,97files: [{98status: 'loaded' as const,99promptPath: createLocalPromptPath('/workspace/.github/instructions/test.instructions.md', 'test.instructions.md'),100}],101sourceFolders: [{102uri: URI.file('/workspace/.github/instructions'),103storage: PromptsStorage.local,104}],105};106107promptsService.getDiscoveryInfo = async type => type === PromptsType.instructions ? discoveryInfo : emptyDiscoveryInfo(type);108willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });109await flushAsyncLogging();110111const instructionsEvent = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery');112assert.ok(instructionsEvent);113const eventId = instructionsEvent.id;114assert.ok(eventId, 'Event should have an ID for resolution');115116const resolved = await chatDebugService.resolveEvent(eventId);117assert.ok(resolved);118assert.strictEqual(resolved.kind, 'fileList');119if (resolved.kind === 'fileList') {120assert.strictEqual(resolved.discoveryType, 'instructions');121assert.strictEqual(resolved.durationInMillis, 11);122assert.strictEqual(resolved.files.length, 1);123assert.strictEqual(resolved.files[0].name, 'test.instructions.md');124assert.strictEqual(resolved.files[0].status, 'loaded');125assert.strictEqual(resolved.sourceFolders?.length, 1);126}127});128129test('should return undefined for unknown event ids', async () => {130disposables.add(instaService.createInstance(PromptsDebugContribution));131132const resolved = await chatDebugService.resolveEvent('nonexistent-id');133assert.strictEqual(resolved, undefined);134});135136test('should assign event id when discoveryInfo is empty', async () => {137disposables.add(instaService.createInstance(PromptsDebugContribution));138139const firedEvents: IChatDebugEvent[] = [];140disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));141142promptsService.getDiscoveryInfo = async type => emptyDiscoveryInfo(type);143willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });144await flushAsyncLogging();145146assert.strictEqual(firedEvents.length, 5);147assert.ok(firedEvents.every(e => e.id !== undefined), 'Events with discovery info should have an id');148});149150test('should handle discoveryInfo with skipped files', async () => {151disposables.add(instaService.createInstance(PromptsDebugContribution));152153const firedEvents: IChatDebugEvent[] = [];154disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));155156const discoveryInfo: IPromptDiscoveryInfo = {157type: PromptsType.instructions,158durationInMillis: 5,159files: [160{161status: 'loaded' as const,162promptPath: createLocalPromptPath('/workspace/.github/instructions/loaded.instructions.md', 'loaded.instructions.md'),163},164{165status: 'skipped' as const,166promptPath: createLocalPromptPath('/workspace/.github/instructions/skipped.instructions.md', 'skipped.instructions.md'),167skipReason: 'disabled',168},169],170};171172promptsService.getDiscoveryInfo = async type => type === PromptsType.instructions ? discoveryInfo : emptyDiscoveryInfo(type);173willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });174await flushAsyncLogging();175176const eventId = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery')!.id!;177const resolved = await chatDebugService.resolveEvent(eventId);178assert.ok(resolved);179if (resolved.kind === 'fileList') {180assert.strictEqual(resolved.files.length, 2);181assert.strictEqual(resolved.files[0].status, 'loaded');182assert.strictEqual(resolved.files[1].status, 'skipped');183assert.strictEqual(resolved.files[1].skipReason, 'disabled');184}185});186187test('should handle level as undefined (defaults to Info)', async () => {188disposables.add(instaService.createInstance(PromptsDebugContribution));189190const firedEvents: IChatDebugEvent[] = [];191disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));192193promptsService.getDiscoveryInfo = async type => emptyDiscoveryInfo(type);194willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });195await flushAsyncLogging();196197const event = firedEvents[0] as IChatDebugGenericEvent;198assert.strictEqual(event.level, ChatDebugLogLevel.Info, 'Default level should be Info');199});200});201202203