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