import { beforeEach, describe, expect, it, vi } from 'vitest' const { handleMock, fromWebContentsMock, getSpeechModelManagerMock, getSpeechSttServiceMock } = vi.hoisted(() => ({ handleMock: vi.fn(), fromWebContentsMock: vi.fn(), getSpeechModelManagerMock: vi.fn(), getSpeechSttServiceMock: vi.fn() })) vi.mock('electron', () => ({ app: { getPath: vi.fn(() => '/tmp/orca-speech-test') }, BrowserWindow: { fromWebContents: fromWebContentsMock }, ipcMain: { handle: handleMock }, systemPreferences: { getMediaAccessStatus: vi.fn(() => 'granted'), askForMediaAccess: vi.fn(() => Promise.resolve(true)) } })) vi.mock('../speech/model-catalog', () => ({ SPEECH_MODEL_CATALOG: [], getCatalogModel: vi.fn(() => ({ id: 'model-1' })) })) vi.mock('../speech/speech-runtime-service', () => ({ getSpeechModelManager: getSpeechModelManagerMock, getSpeechSttService: getSpeechSttServiceMock })) import { registerSpeechHandlers } from './speech' type SpeechDownloadHandler = (event: { sender: { id: number } }, modelId: string) => Promise function getHandler(channel: string): SpeechDownloadHandler { const call = handleMock.mock.calls.find((entry) => entry[0] === channel) if (!call) { throw new Error(`${channel} handler not registered`) } return call[1] as SpeechDownloadHandler } describe('registerSpeechHandlers', () => { beforeEach(() => { handleMock.mockReset() fromWebContentsMock.mockReset() getSpeechModelManagerMock.mockReset() getSpeechSttServiceMock.mockReset() }) it('clears the model download progress callback after completion', async () => { const clearProgressCallback = vi.fn() const progressCallbacks: ((modelId: string, progress: number) => void)[] = [] let resolveDownload: () => void = () => {} const manager = { setProgressCallback: vi.fn((callback: (modelId: string, progress: number) => void) => { progressCallbacks.push(callback) return clearProgressCallback }), downloadModel: vi.fn( () => new Promise((resolve) => { resolveDownload = resolve }) ) } const send = vi.fn() const window = { isDestroyed: vi.fn(() => false), webContents: { send }, once: vi.fn(), off: vi.fn() } getSpeechModelManagerMock.mockReturnValue(manager) fromWebContentsMock.mockReturnValue(window) registerSpeechHandlers({} as never) const pending = getHandler('speech:downloadModel')({ sender: { id: 7 } }, 'model-1') progressCallbacks[0]?.('model-1', 0.5) resolveDownload() await pending expect(send).toHaveBeenCalledWith('speech:downloadProgress', { modelId: 'model-1', progress: 0.5 }) expect(clearProgressCallback).toHaveBeenCalledTimes(1) expect(window.off).toHaveBeenCalledWith('closed', expect.any(Function)) }) it('clears the model download progress callback when the window closes', async () => { const clearProgressCallback = vi.fn() let resolveDownload: () => void = () => {} const closeHandlers: (() => void)[] = [] const manager = { setProgressCallback: vi.fn(() => clearProgressCallback), downloadModel: vi.fn( () => new Promise((resolve) => { resolveDownload = resolve }) ) } const window = { isDestroyed: vi.fn(() => false), webContents: { send: vi.fn() }, once: vi.fn((_event: string, handler: () => void) => { closeHandlers.push(handler) }), off: vi.fn() } getSpeechModelManagerMock.mockReturnValue(manager) fromWebContentsMock.mockReturnValue(window) registerSpeechHandlers({} as never) const pending = getHandler('speech:downloadModel')({ sender: { id: 7 } }, 'model-1') closeHandlers[0]?.() resolveDownload() await pending expect(clearProgressCallback).toHaveBeenCalledTimes(1) expect(window.off).toHaveBeenCalledWith('closed', expect.any(Function)) }) })