diff --git a/web/src/realtime/VoiceBackendSession.test.tsx b/web/src/realtime/VoiceBackendSession.test.tsx new file mode 100644 index 00000000..8c592da1 --- /dev/null +++ b/web/src/realtime/VoiceBackendSession.test.tsx @@ -0,0 +1,62 @@ +import { cleanup, render, waitFor } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { VoiceBackendSession } from '@/realtime/VoiceBackendSession' +import type { ApiClient } from '@/api/client' + +const fetchVoiceBackendMock = vi.fn() + +vi.mock('@/api/voice', () => ({ + fetchVoiceBackend: (api: ApiClient) => fetchVoiceBackendMock(api), +})) + +vi.mock('@/realtime/RealtimeVoiceSession', () => ({ + RealtimeVoiceSession: () => null, +})) + +vi.mock('@/realtime/GeminiLiveVoiceSession', () => ({ + GeminiLiveVoiceSession: () => null, +})) + +vi.mock('@/realtime/QwenVoiceSession', () => ({ + QwenVoiceSession: () => null, +})) + +const api = {} as ApiClient + +describe('VoiceBackendSession', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + cleanup() + }) + + it('does not report a voice error when backend detection fails', async () => { + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const detectionError = new Error('HTTP 404 : 404 Not Found') + fetchVoiceBackendMock.mockRejectedValue(detectionError) + const onStatusChange = vi.fn() + + render() + + await waitFor(() => { + expect(consoleError).toHaveBeenCalledWith(expect.any(String), detectionError) + }) + expect(onStatusChange).not.toHaveBeenCalled() + + consoleError.mockRestore() + }) + + it('reports the detected backend as ready', async () => { + fetchVoiceBackendMock.mockResolvedValue({ backend: 'elevenlabs', backends: ['elevenlabs'] }) + const onStatusChange = vi.fn() + + render() + + await waitFor(() => { + expect(fetchVoiceBackendMock).toHaveBeenCalledWith(api) + }) + expect(onStatusChange).not.toHaveBeenCalled() + }) +}) diff --git a/web/src/realtime/VoiceBackendSession.tsx b/web/src/realtime/VoiceBackendSession.tsx index b1a55b0c..960d17db 100644 --- a/web/src/realtime/VoiceBackendSession.tsx +++ b/web/src/realtime/VoiceBackendSession.tsx @@ -36,8 +36,10 @@ export function VoiceBackendSession(props: VoiceBackendSessionProps) { } }).catch((err: unknown) => { if (!cancelled) { - const msg = err instanceof Error ? err.message : 'Could not detect voice backend' - props.onStatusChange?.('error', msg) + // Detection runs on mount for every session, not in response to a user action, + // so it must not raise the global voice error banner. Leaving `backend` null + // keeps `onReadyChange` false, which disables the voice button. + console.error('Could not detect voice backend:', err) } }) return () => {