From 51c2b4b3f5891de6bffc2b6f9d371b7eabd75f71 Mon Sep 17 00:00:00 2001
From: Haoqing Wang <78337154+hqhq1025@users.noreply.github.com>
Date: Sun, 26 Jul 2026 15:04:59 +0800
Subject: [PATCH] fix(web): stop voice backend detection from raising the error
banner (#1164)
---
web/src/realtime/VoiceBackendSession.test.tsx | 62 +++++++++++++++++++
web/src/realtime/VoiceBackendSession.tsx | 6 +-
2 files changed, 66 insertions(+), 2 deletions(-)
create mode 100644 web/src/realtime/VoiceBackendSession.test.tsx
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 () => {