Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/browser/promptsDebugContribution.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 { Emitter } from '../../../../../base/common/event.js';
8
import { URI } from '../../../../../base/common/uri.js';
9
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
10
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
11
import { ChatDebugLogLevel, IChatDebugEvent, IChatDebugGenericEvent, IChatDebugService } from '../../common/chatDebugService.js';
12
import { ChatDebugServiceImpl } from '../../common/chatDebugServiceImpl.js';
13
import { LocalChatSessionUri } from '../../common/model/chatUri.js';
14
import { IChatAgentService, IChatAgentInvocationEvent } from '../../common/participants/chatAgents.js';
15
import { IChatService } from '../../common/chatService/chatService.js';
16
import { PromptsDebugContribution } from '../../browser/promptsDebugContribution.js';
17
import { ILocalPromptPath, IPromptDiscoveryInfo, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
18
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
19
20
function createLocalPromptPath(path: string, name: string): ILocalPromptPath {
21
return {
22
uri: URI.file(path),
23
name,
24
storage: PromptsStorage.local,
25
type: PromptsType.instructions,
26
};
27
}
28
29
function isGenericEvent(event: IChatDebugEvent): event is IChatDebugGenericEvent {
30
return event.kind === 'generic';
31
}
32
33
async function flushAsyncLogging(): Promise<void> {
34
await new Promise<void>(resolve => setTimeout(resolve, 0));
35
}
36
37
suite('PromptsDebugContribution', () => {
38
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
39
40
let chatDebugService: ChatDebugServiceImpl;
41
let willInvokeAgentEmitter: Emitter<IChatAgentInvocationEvent>;
42
let instaService: TestInstantiationService;
43
let promptsService: Partial<IPromptsService>;
44
const emptyDiscoveryInfo = (type: PromptsType): IPromptDiscoveryInfo => ({ type, files: [], durationInMillis: 0 });
45
46
setup(() => {
47
instaService = disposables.add(new TestInstantiationService());
48
49
chatDebugService = disposables.add(new ChatDebugServiceImpl());
50
instaService.stub(IChatDebugService, chatDebugService);
51
52
willInvokeAgentEmitter = disposables.add(new Emitter<IChatAgentInvocationEvent>());
53
instaService.stub(IChatAgentService, { onWillInvokeAgent: willInvokeAgentEmitter.event } as Partial<IChatAgentService>);
54
instaService.stub(IChatService, { onDidDisposeSession: disposables.add(new Emitter()).event } as Partial<IChatService>);
55
promptsService = {
56
getDiscoveryInfo: async type => emptyDiscoveryInfo(type),
57
};
58
instaService.stub(IPromptsService, promptsService);
59
});
60
61
test('should forward discovery events to chat debug service', async () => {
62
disposables.add(instaService.createInstance(PromptsDebugContribution));
63
64
const firedEvents: IChatDebugEvent[] = [];
65
disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));
66
67
promptsService.getDiscoveryInfo = async type => ({
68
type,
69
durationInMillis: 7,
70
files: type === PromptsType.instructions ? [{
71
status: 'loaded' as const,
72
promptPath: createLocalPromptPath('/workspace/.github/instructions/test.instructions.md', 'test.instructions.md'),
73
}] : [],
74
});
75
76
willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });
77
await flushAsyncLogging();
78
79
assert.strictEqual(firedEvents.length, 5);
80
const event = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery');
81
assert.ok(event);
82
assert.strictEqual(event.kind, 'generic');
83
assert.ok(event.sessionResource);
84
assert.strictEqual(event.name, 'Instructions Discovery');
85
assert.ok(event.details?.includes('Resolved 1 instruction'));
86
assert.strictEqual(event.category, 'discovery');
87
});
88
89
test('should store discoveryInfo and resolve via resolveEvent', async () => {
90
disposables.add(instaService.createInstance(PromptsDebugContribution));
91
92
const firedEvents: IChatDebugEvent[] = [];
93
disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));
94
95
const discoveryInfo: IPromptDiscoveryInfo = {
96
type: PromptsType.instructions,
97
durationInMillis: 11,
98
files: [{
99
status: 'loaded' as const,
100
promptPath: createLocalPromptPath('/workspace/.github/instructions/test.instructions.md', 'test.instructions.md'),
101
}],
102
sourceFolders: [{
103
uri: URI.file('/workspace/.github/instructions'),
104
storage: PromptsStorage.local,
105
}],
106
};
107
108
promptsService.getDiscoveryInfo = async type => type === PromptsType.instructions ? discoveryInfo : emptyDiscoveryInfo(type);
109
willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });
110
await flushAsyncLogging();
111
112
const instructionsEvent = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery');
113
assert.ok(instructionsEvent);
114
const eventId = instructionsEvent.id;
115
assert.ok(eventId, 'Event should have an ID for resolution');
116
117
const resolved = await chatDebugService.resolveEvent(eventId);
118
assert.ok(resolved);
119
assert.strictEqual(resolved.kind, 'fileList');
120
if (resolved.kind === 'fileList') {
121
assert.strictEqual(resolved.discoveryType, 'instructions');
122
assert.strictEqual(resolved.durationInMillis, 11);
123
assert.strictEqual(resolved.files.length, 1);
124
assert.strictEqual(resolved.files[0].name, 'test.instructions.md');
125
assert.strictEqual(resolved.files[0].status, 'loaded');
126
assert.strictEqual(resolved.sourceFolders?.length, 1);
127
}
128
});
129
130
test('should return undefined for unknown event ids', async () => {
131
disposables.add(instaService.createInstance(PromptsDebugContribution));
132
133
const resolved = await chatDebugService.resolveEvent('nonexistent-id');
134
assert.strictEqual(resolved, undefined);
135
});
136
137
test('should assign event id when discoveryInfo is empty', async () => {
138
disposables.add(instaService.createInstance(PromptsDebugContribution));
139
140
const firedEvents: IChatDebugEvent[] = [];
141
disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));
142
143
promptsService.getDiscoveryInfo = async type => emptyDiscoveryInfo(type);
144
willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });
145
await flushAsyncLogging();
146
147
assert.strictEqual(firedEvents.length, 5);
148
assert.ok(firedEvents.every(e => e.id !== undefined), 'Events with discovery info should have an id');
149
});
150
151
test('should handle discoveryInfo with skipped files', async () => {
152
disposables.add(instaService.createInstance(PromptsDebugContribution));
153
154
const firedEvents: IChatDebugEvent[] = [];
155
disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));
156
157
const discoveryInfo: IPromptDiscoveryInfo = {
158
type: PromptsType.instructions,
159
durationInMillis: 5,
160
files: [
161
{
162
status: 'loaded' as const,
163
promptPath: createLocalPromptPath('/workspace/.github/instructions/loaded.instructions.md', 'loaded.instructions.md'),
164
},
165
{
166
status: 'skipped' as const,
167
promptPath: createLocalPromptPath('/workspace/.github/instructions/skipped.instructions.md', 'skipped.instructions.md'),
168
skipReason: 'disabled',
169
},
170
],
171
};
172
173
promptsService.getDiscoveryInfo = async type => type === PromptsType.instructions ? discoveryInfo : emptyDiscoveryInfo(type);
174
willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });
175
await flushAsyncLogging();
176
177
const eventId = firedEvents.find((e): e is IChatDebugGenericEvent => isGenericEvent(e) && e.name === 'Instructions Discovery')!.id!;
178
const resolved = await chatDebugService.resolveEvent(eventId);
179
assert.ok(resolved);
180
if (resolved.kind === 'fileList') {
181
assert.strictEqual(resolved.files.length, 2);
182
assert.strictEqual(resolved.files[0].status, 'loaded');
183
assert.strictEqual(resolved.files[1].status, 'skipped');
184
assert.strictEqual(resolved.files[1].skipReason, 'disabled');
185
}
186
});
187
188
test('should handle level as undefined (defaults to Info)', async () => {
189
disposables.add(instaService.createInstance(PromptsDebugContribution));
190
191
const firedEvents: IChatDebugEvent[] = [];
192
disposables.add(chatDebugService.onDidAddEvent(e => firedEvents.push(e)));
193
194
promptsService.getDiscoveryInfo = async type => emptyDiscoveryInfo(type);
195
willInvokeAgentEmitter.fire({ agentId: 'test-agent', request: { sessionResource: LocalChatSessionUri.forSession('session-1') } as IChatAgentInvocationEvent['request'] });
196
await flushAsyncLogging();
197
198
const event = firedEvents[0] as IChatDebugGenericEvent;
199
assert.strictEqual(event.level, ChatDebugLogLevel.Info, 'Default level should be Info');
200
});
201
});
202
203