Path: blob/main/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts
5302 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/participants/chatAgents.js';15import { IChatModel } from '../../common/model/chatModel.js';16import { IChatFollowup, IChatProgress } from '../../common/chatService/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.Chat];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}51setYieldRequested(requestId: string): void {52}53invoke(request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error('Method not implemented.'); }54metadata = {};55}5657const agents: IChatAgent[] = [58new TestChatAgent('workspace', [59new TestChatAgentCommand('fix', 'fix'),60new TestChatAgentCommand('explain', 'explain')61]),62new TestChatAgent('vscode', [63new TestChatAgentCommand('search', 'search')64]),65];6667class TestChatAgentService implements IChatAgentService {68_serviceBrand: undefined;69readonly onDidChangeAgents = Event.None;70registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable { throw new Error(); }71registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { throw new Error('Method not implemented.'); }72invokeAgent(id: string, request: IChatAgentRequest, progress: (part: IChatProgress[]) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatAgentResult> { throw new Error(); }73setRequestTools(agent: string, requestId: string, tools: UserSelectedTools): void { }74setYieldRequested(agent: string, requestId: string): void { }75getFollowups(id: string, request: IChatAgentRequest, result: IChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<IChatFollowup[]> { throw new Error(); }76getActivatedAgents(): IChatAgent[] { return agents; }77getAgents(): IChatAgent[] { return agents; }78getDefaultAgent(): IChatAgent | undefined { throw new Error(); }79getContributedDefaultAgent(): IChatAgentData | undefined { throw new Error(); }80registerAgent(id: string, data: IChatAgentData): IDisposable { throw new Error('Method not implemented.'); }81getAgent(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }82getAgentsByName(name: string): IChatAgentData[] { throw new Error('Method not implemented.'); }83updateAgent(id: string, updateMetadata: IChatAgentMetadata): void { throw new Error('Method not implemented.'); }84getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { throw new Error('Method not implemented.'); }85registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise<IChatAgentCompletionItem[]>): IDisposable { throw new Error('Method not implemented.'); }86getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise<IChatAgentCompletionItem[]> { throw new Error('Method not implemented.'); }87agentHasDupeName(id: string): boolean { throw new Error('Method not implemented.'); }88getChatTitle(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }89getChatSummary(id: string, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise<string | undefined> { throw new Error('Method not implemented.'); }90hasToolsAgent: boolean = false;91hasChatParticipantDetectionProviders(): boolean {92throw new Error('Method not implemented.');93}94registerChatParticipantDetectionProvider(handle: number, provider: IChatParticipantDetectionProvider): IDisposable {95throw new Error('Method not implemented.');96}97detectAgentOrCommand(request: IChatAgentRequest, history: IChatAgentHistoryEntry[], options: { location: ChatAgentLocation }, token: CancellationToken): Promise<{ agent: IChatAgentData; command?: IChatAgentCommand } | undefined> {98throw new Error('Method not implemented.');99}100}101102class TestSpeechService implements ISpeechService {103_serviceBrand: undefined;104105onDidChangeHasSpeechProvider = Event.None;106107readonly hasSpeechProvider = true;108readonly hasActiveSpeechToTextSession = false;109readonly hasActiveTextToSpeechSession = false;110readonly hasActiveKeywordRecognition = false;111112registerSpeechProvider(identifier: string, provider: ISpeechProvider): IDisposable { throw new Error('Method not implemented.'); }113onDidStartSpeechToTextSession = Event.None;114onDidEndSpeechToTextSession = Event.None;115116async createSpeechToTextSession(token: CancellationToken): Promise<ISpeechToTextSession> {117return {118onDidChange: emitter.event119};120}121122onDidStartTextToSpeechSession = Event.None;123onDidEndTextToSpeechSession = Event.None;124125async createTextToSpeechSession(token: CancellationToken): Promise<ITextToSpeechSession> {126return {127onDidChange: Event.None,128synthesize: async () => { }129};130}131132onDidStartKeywordRecognition = Event.None;133onDidEndKeywordRecognition = Event.None;134recognizeKeyword(token: CancellationToken): Promise<KeywordRecognitionStatus> { throw new Error('Method not implemented.'); }135}136137const disposables = new DisposableStore();138let emitter: Emitter<ISpeechToTextEvent>;139140let service: VoiceChatService;141let event: IVoiceChatTextEvent | undefined;142143async function createSession(options: IVoiceChatSessionOptions) {144const cts = new CancellationTokenSource();145disposables.add(toDisposable(() => cts.dispose(true)));146const session = await service.createVoiceChatSession(cts.token, options);147disposables.add(session.onDidChange(e => {148event = e;149}));150}151152setup(() => {153emitter = disposables.add(new Emitter<ISpeechToTextEvent>());154service = disposables.add(new VoiceChatService(new TestSpeechService(), new TestChatAgentService(), new MockContextKeyService()));155});156157teardown(() => {158disposables.clear();159});160161test('Agent and slash command detection (useAgents: false)', async () => {162await testAgentsAndSlashCommandsDetection({ usesAgents: false, model: {} as IChatModel });163});164165test('Agent and slash command detection (useAgents: true)', async () => {166await testAgentsAndSlashCommandsDetection({ usesAgents: true, model: {} as IChatModel });167});168169async function testAgentsAndSlashCommandsDetection(options: IVoiceChatSessionOptions) {170171// Nothing to detect172await createSession(options);173174emitter.fire({ status: SpeechToTextStatus.Started });175assert.strictEqual(event?.status, SpeechToTextStatus.Started);176177emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello' });178assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);179assert.strictEqual(event?.text, 'Hello');180assert.strictEqual(event?.waitingForInput, undefined);181182emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Hello World' });183assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);184assert.strictEqual(event?.text, 'Hello World');185assert.strictEqual(event?.waitingForInput, undefined);186187emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Hello World' });188assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);189assert.strictEqual(event?.text, 'Hello World');190assert.strictEqual(event?.waitingForInput, undefined);191192// Agent193await createSession(options);194195emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At' });196assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);197assert.strictEqual(event?.text, 'At');198199emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });200assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);201assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'At workspace');202assert.strictEqual(event?.waitingForInput, options.usesAgents);203204emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'at workspace' });205assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);206assert.strictEqual(event?.text, options.usesAgents ? '@workspace' : 'at workspace');207assert.strictEqual(event?.waitingForInput, options.usesAgents);208209emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace help' });210assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);211assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');212assert.strictEqual(event?.waitingForInput, false);213214emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace help' });215assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);216assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace help');217assert.strictEqual(event?.waitingForInput, false);218219// Agent with punctuation220await createSession(options);221222emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, help' });223assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);224assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');225assert.strictEqual(event?.waitingForInput, false);226227emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, help' });228assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);229assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At workspace, help');230assert.strictEqual(event?.waitingForInput, false);231232await createSession(options);233234emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At Workspace. help' });235assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);236assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');237assert.strictEqual(event?.waitingForInput, false);238239emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At Workspace. help' });240assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);241assert.strictEqual(event?.text, options.usesAgents ? '@workspace help' : 'At Workspace. help');242assert.strictEqual(event?.waitingForInput, false);243244// Slash Command245await createSession(options);246247emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'Slash fix' });248assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);249assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');250assert.strictEqual(event?.waitingForInput, true);251252emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'Slash fix' });253assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);254assert.strictEqual(event?.text, options.usesAgents ? '@workspace /fix' : '/fix');255assert.strictEqual(event?.waitingForInput, true);256257// Agent + Slash Command258await createSession(options);259260emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code slash search help' });261assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);262assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');263assert.strictEqual(event?.waitingForInput, false);264265emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code slash search help' });266assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);267assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code slash search help');268assert.strictEqual(event?.waitingForInput, false);269270// Agent + Slash Command with punctuation271await createSession(options);272273emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code, slash search, help' });274assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);275assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');276assert.strictEqual(event?.waitingForInput, false);277278emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code, slash search, help' });279assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);280assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code, slash search, help');281assert.strictEqual(event?.waitingForInput, false);282283await createSession(options);284285emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At code. slash, search help' });286assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);287assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash, search help');288assert.strictEqual(event?.waitingForInput, false);289290emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At code. slash search, help' });291assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);292assert.strictEqual(event?.text, options.usesAgents ? '@vscode /search help' : 'At code. slash search, help');293assert.strictEqual(event?.waitingForInput, false);294295// Agent not detected twice296await createSession(options);297298emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace, for at workspace' });299assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);300assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');301assert.strictEqual(event?.waitingForInput, false);302303emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace, for at workspace' });304assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);305assert.strictEqual(event?.text, options.usesAgents ? '@workspace for at workspace' : 'At workspace, for at workspace');306assert.strictEqual(event?.waitingForInput, false);307308// Slash command detected after agent recognized309if (options.usesAgents) {310await createSession(options);311312emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });313assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);314assert.strictEqual(event?.text, '@workspace');315assert.strictEqual(event?.waitingForInput, true);316317emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash' });318assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);319assert.strictEqual(event?.text, 'slash');320assert.strictEqual(event?.waitingForInput, false);321322emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'slash fix' });323assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);324assert.strictEqual(event?.text, '/fix');325assert.strictEqual(event?.waitingForInput, true);326327emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });328assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);329assert.strictEqual(event?.text, '/fix');330assert.strictEqual(event?.waitingForInput, true);331332await createSession(options);333334emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });335assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);336assert.strictEqual(event?.text, '@workspace');337assert.strictEqual(event?.waitingForInput, true);338339emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'slash fix' });340assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);341assert.strictEqual(event?.text, '/fix');342assert.strictEqual(event?.waitingForInput, true);343}344}345346test('waiting for input', async () => {347348// Agent349await createSession({ usesAgents: true, model: {} as IChatModel });350351emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace' });352assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);353assert.strictEqual(event?.text, '@workspace');354assert.strictEqual(event.waitingForInput, true);355356emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace' });357assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);358assert.strictEqual(event?.text, '@workspace');359assert.strictEqual(event.waitingForInput, true);360361// Slash Command362await createSession({ usesAgents: true, model: {} as IChatModel });363364emitter.fire({ status: SpeechToTextStatus.Recognizing, text: 'At workspace slash explain' });365assert.strictEqual(event?.status, SpeechToTextStatus.Recognizing);366assert.strictEqual(event?.text, '@workspace /explain');367assert.strictEqual(event.waitingForInput, true);368369emitter.fire({ status: SpeechToTextStatus.Recognized, text: 'At workspace slash explain' });370assert.strictEqual(event?.status, SpeechToTextStatus.Recognized);371assert.strictEqual(event?.text, '@workspace /explain');372assert.strictEqual(event.waitingForInput, true);373});374375ensureNoDisposablesAreLeakedInTestSuite();376});377378379