Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts
3296 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 { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';
8
import { Emitter, Event } from '../../../../../base/common/event.js';
9
import { DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
10
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
11
import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';
12
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
13
import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
14
import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from '../../../speech/common/speechService.js';
15
import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider, UserSelectedTools } from '../../common/chatAgents.js';
16
import { IChatModel } from '../../common/chatModel.js';
17
import { IChatFollowup, IChatProgress } from '../../common/chatService.js';
18
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
19
import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from '../../common/voiceChatService.js';
20
21
suite('VoiceChat', () => {
22
23
class TestChatAgentCommand implements IChatAgentCommand {
24
constructor(readonly name: string, readonly description: string) { }
25
}
26
27
class TestChatAgent implements IChatAgent {
28
29
extensionId: ExtensionIdentifier = nullExtensionDescription.identifier;
30
extensionVersion: string | undefined = undefined;
31
extensionPublisher = '';
32
extensionDisplayName = '';
33
extensionPublisherId = '';
34
locations: ChatAgentLocation[] = [ChatAgentLocation.Panel];
35
modes = [ChatModeKind.Ask];
36
public readonly name: string;
37
constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) {
38
this.name = id;
39
}
40
fullName?: string | undefined;
41
description?: string | undefined;
42
when?: string | undefined;
43
publisherDisplayName?: string | undefined;
44
isDefault?: boolean | undefined;
45
isDynamic?: boolean | undefined;
46
disambiguation: { category: string; description: string; examples: string[] }[] = [];
47
provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> {
48
throw new Error('Method not implemented.');
49
}
50
setRequestTools(requestId: string, tools: UserSelectedTools): void {
51
}
52
invoke(request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); }
53
metadata = {};
54
}
55
56
const agents: IChatAgent[] = [
57
new TestChatAgent('workspace', [
58
new TestChatAgentCommand('fix', 'fix'),
59
new TestChatAgentCommand('explain', 'explain')
60
]),
61
new TestChatAgent('vscode', [
62
new TestChatAgentCommand('search', 'search')
63
]),
64
];
65
66
class TestChatAgentService implements IChatAgentService {
67
_serviceBrand: undefined;
68
readonly onDidChangeAgents = Event.None;
69
registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }
70
registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); }
71
invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }
72
setRequestTools(agent: string, requestId: string, tools: UserSelectedTools): void { }
73
getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }
74
getActivatedAgents(): IChatAgent[] { return agents; }
75
getAgents(): IChatAgent[] { return agents; }
76
getDefaultAgent(): IChatAgent | undefined { throw new Error(); }
77
getContributedDefaultAgent(): IChatAgentData | undefined { throw new Error(); }
78
registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); }
79
getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }
80
getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); }
81
updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); }
82
getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }
83
registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise<IChatAgentCompletionItem[]>): IDisposable { throw new Error('Method not implemented.'); }
84
getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]> { throw new Error('Method not implemented.'); }
85
agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); }
86
getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }
87
getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }
88
hasToolsAgent: boolean = false;
89
hasChatParticipantDetectionProviders(): boolean {
90
throw new Error('Method not implemented.');
91
}
92
registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable {
93
throw new Error('Method not implemented.');
94
}
95
detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> {
96
throw new Error('Method not implemented.');
97
}
98
}
99
100
class TestSpeechService implements ISpeechService {
101
_serviceBrand: undefined;
102
103
onDidChangeHasSpeechProvider = Event.None;
104
105
readonly hasSpeechProvider = true;
106
readonly hasActiveSpeechToTextSession = false;
107
readonly hasActiveTextToSpeechSession = false;
108
readonly hasActiveKeywordRecognition = false;
109
110
registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { throw new Error('Method not implemented.'); }
111
onDidStartSpeechToTextSession = Event.None;
112
onDidEndSpeechToTextSession = Event.None;
113
114
async createSpeechToTextSession(token: CancellationToken): Promise<ISpeechToTextSession> {
115
return {
116
onDidChange: emitter.event
117
};
118
}
119
120
onDidStartTextToSpeechSession = Event.None;
121
onDidEndTextToSpeechSession = Event.None;
122
123
async createTextToSpeechSession(token: CancellationToken): Promise<ITextToSpeechSession> {
124
return {
125
onDidChange: Event.None,
126
synthesize: async () => { }
127
};
128
}
129
130
onDidStartKeywordRecognition = Event.None;
131
onDidEndKeywordRecognition = Event.None;
132
recognizeKeyword(token: CancellationToken): Promise<KeywordRecognitionStatus> { throw new Error('Method not implemented.'); }
133
}
134
135
const disposables = new DisposableStore();
136
let emitter: Emitter<ISpeechToTextEvent>;
137
138
let service: VoiceChatService;
139
let event: IVoiceChatTextEvent | undefined;
140
141
async function createSession(options: IVoiceChatSessionOptions) {
142
const cts = new CancellationTokenSource();
143
disposables.add(toDisposable(() => cts.dispose(true)));
144
const session = await service.createVoiceChatSession(cts.token, options);
145
disposables.add(session.onDidChange(e => {
146
event = e;
147
}));
148
}
149
150
setup(() => {
151
emitter = disposables.add(new Emitter<ISpeechToTextEvent>());
152
service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService()));
153
});
154
155
teardown(() => {
156
disposables.clear();
157
});
158
159
test('Agent and slash command detection (useAgents: false)', async () => {
160
await testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel });
161
});
162
163
test('Agent and slash command detection (useAgents: true)', async () => {
164
await testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel });
165
});
166
167
async function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) {
168
169
// Nothing to detect
170
await createSession(options);
171
172
emitter.fire({ status: SpeechToTextStatus.Started });
173
assert.strictEqual(event?.status, SpeechToTextStatus.Started);
174
175
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello' });
176
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
177
assert.strictEqual(event?.text, 'Hello');
178
assert.strictEqual(event?.waitingForInput, undefined);
179
180
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello World' });
181
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
182
assert.strictEqual(event?.text, 'Hello World');
183
assert.strictEqual(event?.waitingForInput, undefined);
184
185
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Hello World' });
186
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
187
assert.strictEqual(event?.text, 'Hello World');
188
assert.strictEqual(event?.waitingForInput, undefined);
189
190
// Agent
191
await createSession(options);
192
193
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At' });
194
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
195
assert.strictEqual(event?.text, 'At');
196
197
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });
198
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
199
assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'At workspace');
200
assert.strictEqual(event?.waitingForInput, options.usesAgents);
201
202
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'at workspace' });
203
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
204
assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'at workspace');
205
assert.strictEqual(event?.waitingForInput, options.usesAgents);
206
207
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace help' });
208
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
209
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');
210
assert.strictEqual(event?.waitingForInput, false);
211
212
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace help' });
213
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
214
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');
215
assert.strictEqual(event?.waitingForInput, false);
216
217
// Agent with punctuation
218
await createSession(options);
219
220
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, help' });
221
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
222
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');
223
assert.strictEqual(event?.waitingForInput, false);
224
225
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, help' });
226
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
227
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');
228
assert.strictEqual(event?.waitingForInput, false);
229
230
await createSession(options);
231
232
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At Workspace. help' });
233
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
234
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');
235
assert.strictEqual(event?.waitingForInput, false);
236
237
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At Workspace. help' });
238
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
239
assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');
240
assert.strictEqual(event?.waitingForInput, false);
241
242
// Slash Command
243
await createSession(options);
244
245
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Slash fix' });
246
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
247
assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');
248
assert.strictEqual(event?.waitingForInput, true);
249
250
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Slash fix' });
251
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
252
assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');
253
assert.strictEqual(event?.waitingForInput, true);
254
255
// Agent + Slash Command
256
await createSession(options);
257
258
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code slash search help' });
259
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
260
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');
261
assert.strictEqual(event?.waitingForInput, false);
262
263
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code slash search help' });
264
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
265
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');
266
assert.strictEqual(event?.waitingForInput, false);
267
268
// Agent + Slash Command with punctuation
269
await createSession(options);
270
271
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code, slash search, help' });
272
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
273
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');
274
assert.strictEqual(event?.waitingForInput, false);
275
276
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code, slash search, help' });
277
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
278
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');
279
assert.strictEqual(event?.waitingForInput, false);
280
281
await createSession(options);
282
283
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code. slash, search help' });
284
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
285
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash, search help');
286
assert.strictEqual(event?.waitingForInput, false);
287
288
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code. slash search, help' });
289
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
290
assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash search, help');
291
assert.strictEqual(event?.waitingForInput, false);
292
293
// Agent not detected twice
294
await createSession(options);
295
296
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, for at workspace' });
297
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
298
assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');
299
assert.strictEqual(event?.waitingForInput, false);
300
301
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, for at workspace' });
302
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
303
assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');
304
assert.strictEqual(event?.waitingForInput, false);
305
306
// Slash command detected after agent recognized
307
if (options.usesAgents) {
308
await createSession(options);
309
310
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });
311
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
312
assert.strictEqual(event?.text, '@workspace');
313
assert.strictEqual(event?.waitingForInput, true);
314
315
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash' });
316
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
317
assert.strictEqual(event?.text, 'slash');
318
assert.strictEqual(event?.waitingForInput, false);
319
320
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash fix' });
321
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
322
assert.strictEqual(event?.text, '/fix');
323
assert.strictEqual(event?.waitingForInput, true);
324
325
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });
326
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
327
assert.strictEqual(event?.text, '/fix');
328
assert.strictEqual(event?.waitingForInput, true);
329
330
await createSession(options);
331
332
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });
333
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
334
assert.strictEqual(event?.text, '@workspace');
335
assert.strictEqual(event?.waitingForInput, true);
336
337
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });
338
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
339
assert.strictEqual(event?.text, '/fix');
340
assert.strictEqual(event?.waitingForInput, true);
341
}
342
}
343
344
test('waiting for input', async () => {
345
346
// Agent
347
await createSession({ usesAgents: true, model: {} as IChatModel });
348
349
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });
350
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
351
assert.strictEqual(event?.text, '@workspace');
352
assert.strictEqual(event.waitingForInput, true);
353
354
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });
355
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
356
assert.strictEqual(event?.text, '@workspace');
357
assert.strictEqual(event.waitingForInput, true);
358
359
// Slash Command
360
await createSession({ usesAgents: true, model: {} as IChatModel });
361
362
emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace slash explain' });
363
assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);
364
assert.strictEqual(event?.text, '@workspace /explain');
365
assert.strictEqual(event.waitingForInput, true);
366
367
emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace slash explain' });
368
assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);
369
assert.strictEqual(event?.text, '@workspace /explain');
370
assert.strictEqual(event.waitingForInput, true);
371
});
372
373
ensureNoDisposablesAreLeakedInTestSuite();
374
});
375
376