From f739e2246de8d6b272cda106f94c76d7d181aa85 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 12:52:37 -0700 Subject: [PATCH] fix: surface speech model download failures (#3880) --- .../model-manager-download-error.test.ts | 70 +++++++++++++++++++ src/main/speech/model-manager.ts | 5 ++ 2 files changed, 75 insertions(+) create mode 100644 src/main/speech/model-manager-download-error.test.ts diff --git a/src/main/speech/model-manager-download-error.test.ts b/src/main/speech/model-manager-download-error.test.ts new file mode 100644 index 000000000..14c33908e --- /dev/null +++ b/src/main/speech/model-manager-download-error.test.ts @@ -0,0 +1,70 @@ +import { mkdtempSync, rmSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { SPEECH_MODEL_CATALOG } from './model-catalog' +import { ModelManager } from './model-manager' + +const { httpsGetMock } = vi.hoisted(() => ({ + httpsGetMock: vi.fn() +})) + +vi.mock('electron', () => ({ + app: { + getPath: () => '/tmp/orca-speech-models-test' + } +})) + +vi.mock('https', async () => { + const actual = await vi.importActual('https') + return { ...(actual as Record), get: httpsGetMock } +}) + +describe('ModelManager download failures', () => { + beforeEach(() => { + httpsGetMock.mockReset() + }) + + it('rejects failed model downloads so the caller can surface the error', async () => { + const dir = mkdtempSync(join(tmpdir(), 'orca-model-manager-')) + try { + const manifest = SPEECH_MODEL_CATALOG[0] + const errorHandlers: ((err: Error) => void)[] = [] + const request = { + destroy: vi.fn(() => request), + setTimeout: vi.fn(() => request), + on: vi.fn((event: string, cb: (err: Error) => void) => { + if (event === 'error') { + errorHandlers.push(cb) + } + return request + }), + off: vi.fn((event: string, cb: (err: Error) => void) => { + if (event === 'error') { + const index = errorHandlers.indexOf(cb) + if (index !== -1) { + errorHandlers.splice(index, 1) + } + } + return request + }) + } + httpsGetMock.mockImplementation(() => { + queueMicrotask(() => { + for (const handler of errorHandlers) { + handler(new Error('network down')) + } + }) + return request + }) + const manager = new ModelManager(dir) + + await expect(manager.downloadModel(manifest.id)).rejects.toThrow('network down') + + expect(request.off).toHaveBeenCalledWith('error', expect.any(Function)) + expect(errorHandlers).toHaveLength(0) + } finally { + rmSync(dir, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/speech/model-manager.ts b/src/main/speech/model-manager.ts index 1b25fd63a..8eb35844e 100644 --- a/src/main/speech/model-manager.ts +++ b/src/main/speech/model-manager.ts @@ -179,6 +179,11 @@ export class ModelManager { this.updateState(modelId, 'error', undefined, String(err)) } this.cleanup(modelId, archivePath) + if (!aborted) { + // Why: the settings UI awaits this promise to show download failures; + // cancellation stays quiet, but real failures must reach the caller. + throw err + } } finally { this.activeDownloads.delete(modelId) try {