Path: blob/main/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts
3296 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 { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';9import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';10import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js';11import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';12import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';13import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from '../../../speech/common/speechService.js';14import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider, UserSelectedTools } from '../../common/chatAgents.js';15import { IChatModel } from '../../common/chatModel.js';16import { IChatFollowup, IChatProgress } from '../../common/chatService.js';17import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';18import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from '../../common/voiceChatService.js';1920suite('VoiceChat', () => {2122class TestChatAgentCommand implements IChatAgentCommand {23constructor(readonly name: string, readonly description: string) { }24}2526class TestChatAgent implements IChatAgent {2728extensionId: ExtensionIdentifier = nullExtensionDescription.identifier;29extensionVersion: string | undefined = undefined;30extensionPublisher = '';31extensionDisplayName = '';32extensionPublisherId = '';33locations: ChatAgentLocation[] = [ChatAgentLocation.Panel];34modes = [ChatModeKind.Ask];35public readonly name: string;36constructor(readonly id: string, readonly slashCommands: IChatAgentCommand[]) {37this.name = id;38}39fullName?: string | undefined;40description?: string | undefined;41when?: string | undefined;42publisherDisplayName?: string | undefined;43isDefault?: boolean | undefined;44isDynamic?: boolean | undefined;45disambiguation: { category: string; description: string; examples: string[] }[] = [];46provideFollowups?(request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> {47throw new Error('Method not implemented.');48}49setRequestTools(requestId: string, tools: UserSelectedTools): void {50}51invoke(request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); }52metadata = {};53}5455const agents: IChatAgent[] = [56new TestChatAgent('workspace', [57new TestChatAgentCommand('fix', 'fix'),58new TestChatAgentCommand('explain', 'explain')59]),60new TestChatAgent('vscode', [61new TestChatAgentCommand('search', 'search')62]),63];6465class TestChatAgentService implements IChatAgentService {66_serviceBrand: undefined;67readonly onDidChangeAgents = Event.None;68registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }69registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); }70invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }71setRequestTools(agent: string, requestId: string, tools: UserSelectedTools): void { }72getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }73getActivatedAgents(): IChatAgent[] { return agents; }74getAgents(): IChatAgent[] { return agents; }75getDefaultAgent(): IChatAgent | undefined { throw new Error(); }76getContributedDefaultAgent(): IChatAgentData | undefined { throw new Error(); }77registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); }78getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }79getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); }80updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); }81getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }82registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise<IChatAgentCompletionItem[]>): IDisposable { throw new Error('Method not implemented.'); }83getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]> { throw new Error('Method not implemented.'); }84agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); }85getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }86getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }87hasToolsAgent: boolean = false;88hasChatParticipantDetectionProviders(): boolean {89throw new Error('Method not implemented.');90}91registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable {92throw new Error('Method not implemented.');93}94detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> {95throw new Error('Method not implemented.');96}97}9899class TestSpeechService implements ISpeechService {100_serviceBrand: undefined;101102onDidChangeHasSpeechProvider = Event.None;103104readonly hasSpeechProvider = true;105readonly hasActiveSpeechToTextSession = false;106readonly hasActiveTextToSpeechSession = false;107readonly hasActiveKeywordRecognition = false;108109registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { throw new Error('Method not implemented.'); }110onDidStartSpeechToTextSession = Event.None;111onDidEndSpeechToTextSession = Event.None;112113async createSpeechToTextSession(token: CancellationToken): Promise<ISpeechToTextSession> {114return {115onDidChange: emitter.event116};117}118119onDidStartTextToSpeechSession = Event.None;120onDidEndTextToSpeechSession = Event.None;121122async createTextToSpeechSession(token: CancellationToken): Promise<ITextToSpeechSession> {123return {124onDidChange: Event.None,125synthesize: async () => { }126};127}128129onDidStartKeywordRecognition = Event.None;130onDidEndKeywordRecognition = Event.None;131recognizeKeyword(token: CancellationToken): Promise<KeywordRecognitionStatus> { throw new Error('Method not implemented.'); }132}133134const disposables = new DisposableStore();135let emitter: Emitter<ISpeechToTextEvent>;136137let service: VoiceChatService;138let event: IVoiceChatTextEvent | undefined;139140async function createSession(options: IVoiceChatSessionOptions) {141const cts = new CancellationTokenSource();142disposables.add(toDisposable(() => cts.dispose(true)));143const session = await service.createVoiceChatSession(cts.token, options);144disposables.add(session.onDidChange(e => {145event = e;146}));147}148149setup(() => {150emitter = disposables.add(new Emitter<ISpeechToTextEvent>());151service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService()));152});153154teardown(() => {155disposables.clear();156});157158test('Agent and slash command detection (useAgents: false)', async () => {159await testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel });160});161162test('Agent and slash command detection (useAgents: true)', async () => {163await testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel });164});165166async function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) {167168// Nothing to detect169await createSession(options);170171emitter.fire({ status: SpeechToTextStatus.Started });172assert.strictEqual(event?.status, SpeechToTextStatus.Started);173174emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello' });175assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);176assert.strictEqual(event?.text, 'Hello');177assert.strictEqual(event?.waitingForInput, undefined);178179emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello World' });180assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);181assert.strictEqual(event?.text, 'Hello World');182assert.strictEqual(event?.waitingForInput, undefined);183184emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Hello World' });185assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);186assert.strictEqual(event?.text, 'Hello World');187assert.strictEqual(event?.waitingForInput, undefined);188189// Agent190await createSession(options);191192emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At' });193assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);194assert.strictEqual(event?.text, 'At');195196emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });197assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);198assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'At workspace');199assert.strictEqual(event?.waitingForInput, options.usesAgents);200201emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'at workspace' });202assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);203assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'at workspace');204assert.strictEqual(event?.waitingForInput, options.usesAgents);205206emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace help' });207assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);208assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');209assert.strictEqual(event?.waitingForInput, false);210211emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace help' });212assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);213assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');214assert.strictEqual(event?.waitingForInput, false);215216// Agent with punctuation217await createSession(options);218219emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, help' });220assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);221assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');222assert.strictEqual(event?.waitingForInput, false);223224emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, help' });225assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);226assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');227assert.strictEqual(event?.waitingForInput, false);228229await createSession(options);230231emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At Workspace. help' });232assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);233assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');234assert.strictEqual(event?.waitingForInput, false);235236emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At Workspace. help' });237assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);238assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');239assert.strictEqual(event?.waitingForInput, false);240241// Slash Command242await createSession(options);243244emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Slash fix' });245assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);246assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');247assert.strictEqual(event?.waitingForInput, true);248249emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Slash fix' });250assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);251assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');252assert.strictEqual(event?.waitingForInput, true);253254// Agent + Slash Command255await createSession(options);256257emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code slash search help' });258assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);259assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');260assert.strictEqual(event?.waitingForInput, false);261262emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code slash search help' });263assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);264assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');265assert.strictEqual(event?.waitingForInput, false);266267// Agent + Slash Command with punctuation268await createSession(options);269270emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code, slash search, help' });271assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);272assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');273assert.strictEqual(event?.waitingForInput, false);274275emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code, slash search, help' });276assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);277assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');278assert.strictEqual(event?.waitingForInput, false);279280await createSession(options);281282emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code. slash, search help' });283assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);284assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash, search help');285assert.strictEqual(event?.waitingForInput, false);286287emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code. slash search, help' });288assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);289assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash search, help');290assert.strictEqual(event?.waitingForInput, false);291292// Agent not detected twice293await createSession(options);294295emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, for at workspace' });296assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);297assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');298assert.strictEqual(event?.waitingForInput, false);299300emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, for at workspace' });301assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);302assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');303assert.strictEqual(event?.waitingForInput, false);304305// Slash command detected after agent recognized306if (options.usesAgents) {307await createSession(options);308309emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });310assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);311assert.strictEqual(event?.text, '@workspace');312assert.strictEqual(event?.waitingForInput, true);313314emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash' });315assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);316assert.strictEqual(event?.text, 'slash');317assert.strictEqual(event?.waitingForInput, false);318319emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash fix' });320assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);321assert.strictEqual(event?.text, '/fix');322assert.strictEqual(event?.waitingForInput, true);323324emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });325assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);326assert.strictEqual(event?.text, '/fix');327assert.strictEqual(event?.waitingForInput, true);328329await createSession(options);330331emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });332assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);333assert.strictEqual(event?.text, '@workspace');334assert.strictEqual(event?.waitingForInput, true);335336emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });337assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);338assert.strictEqual(event?.text, '/fix');339assert.strictEqual(event?.waitingForInput, true);340}341}342343test('waiting for input', async () => {344345// Agent346await createSession({ usesAgents: true, model: {} as IChatModel });347348emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });349assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);350assert.strictEqual(event?.text, '@workspace');351assert.strictEqual(event.waitingForInput, true);352353emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });354assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);355assert.strictEqual(event?.text, '@workspace');356assert.strictEqual(event.waitingForInput, true);357358// Slash Command359await createSession({ usesAgents: true, model: {} as IChatModel });360361emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace slash explain' });362assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);363assert.strictEqual(event?.text, '@workspace /explain');364assert.strictEqual(event.waitingForInput, true);365366emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace slash explain' });367assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);368assert.strictEqual(event?.text, '@workspace /explain');369assert.strictEqual(event.waitingForInput, true);370});371372ensureNoDisposablesAreLeakedInTestSuite();373});374375376