Files
hapi/shared/src/voice.backends.test.ts
T
SSU-WEI HUANGandGitHub d396e9d6d4 feat(voice): curate dictation credential presets to ElevenLabs, OpenAI, Groq (#1474)
* feat(voice): curate dictation credential presets to ElevenLabs, OpenAI, Groq

Groq transcription was already wired end-to-end (GROQ_API_KEY,
whisper-large-v3, standard mode), but the credential onboarding panel
listed five providers with no hint that Groq is supported, so mobile
users could not discover it.

- Curate Settings > Voice > Dictation credential presets to ElevenLabs,
  OpenAI, and Groq (Deepgram / OpenAI-compatible remain fully supported
  via env and stay listed when configured)
- Name the three presets in the empty-state and manage hints (en + zh-CN)
- Lock the curated list in with a web preset test, a hub route test for
  the Groq whisper-large-v3 proxy, and shared provider-listing coverage
- Note the presets and no-restart save behavior in voice-assistant.md

Verified: bun typecheck (cli+web+hub) and targeted suites pass; full
test gate green except pre-existing load-sensitive runner stress tests.

* fix(voice): keep legacy dictation providers manageable when configured

HAPI Bot review finding (Major): curating the onboard panel to the three
presets made settings-managed Deepgram / OpenAI-compatible credentials
impossible to rotate or clear from the UI.

- Re-add deepgram / openai-compatible to the onboard provider list
  conditionally when credentials exist, restoring update/clear controls
- Fall back to the first preset if the selected provider leaves the list
- Cover the conditional list in the preset test

* fix(voice): surface partial OpenAI-compatible credentials in onboard panel

HAPI Bot follow-up finding (Major): hub marks openaiCompatible.configured
only when both base URL and model exist, so api-key-only or endpoint-only
stored settings lost the UI path to rotate or clear them.

- Gate the openai-compatible onboard entry on any stored field (base URL,
  model, or API key) via hasOpenAICompatibleCredentials()
- Cover api-key-only / base-url-only / model-only cases in tests
2026-08-11 22:27:44 +08:00

94 lines
3.5 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
buildVoiceAgentConfig,
listConfiguredTranscriptionProviders,
listConfiguredVoiceBackends,
resolveEffectiveVoiceBackend,
resolveHubVoiceBackend
} from './voice'
describe('listConfiguredTranscriptionProviders', () => {
test('returns configured providers with honest mode capabilities', () => {
expect(listConfiguredTranscriptionProviders({
OPENAI_API_KEY: 'openai',
ELEVENLABS_API_KEY: 'elevenlabs',
DEEPGRAM_API_KEY: 'deepgram',
GROQ_API_KEY: 'groq',
TRANSCRIPTION_BASE_URL: 'http://localhost:8000/v1',
TRANSCRIPTION_MODEL: 'whisper-large-v3'
})).toEqual([
{ id: 'openai', label: 'OpenAI', modes: ['standard', 'realtime'] },
{ id: 'elevenlabs', label: 'ElevenLabs', modes: ['standard', 'realtime'] },
{ id: 'deepgram', label: 'Deepgram', modes: ['standard', 'realtime'] },
{ id: 'groq', label: 'Groq', modes: ['standard'] },
{ id: 'openai-compatible', label: 'OpenAI-compatible / local', modes: ['standard'] }
])
})
test('does not advertise incomplete or missing configuration', () => {
expect(listConfiguredTranscriptionProviders({ TRANSCRIPTION_BASE_URL: 'http://localhost:8000/v1' })).toEqual([])
})
})
describe('listConfiguredVoiceBackends', () => {
test('returns only backends with API keys', () => {
const backends = listConfiguredVoiceBackends({
ELEVENLABS_API_KEY: 'el',
GEMINI_API_KEY: 'gm',
DASHSCOPE_API_KEY: 'qw'
})
expect(backends).toEqual(['elevenlabs', 'gemini-live', 'qwen-realtime'])
})
test('returns empty when no keys configured', () => {
expect(listConfiguredVoiceBackends({})).toEqual([])
})
})
describe('buildVoiceAgentConfig', () => {
test('includes ElevenLabs session context placeholder for dynamicVariables', () => {
const prompt = buildVoiceAgentConfig().conversation_config.agent.prompt.prompt
expect(prompt).toContain('{{initialConversationContext}}')
})
})
describe('resolveHubVoiceBackend', () => {
test('uses VOICE_BACKEND when that backend is configured', () => {
const backend = resolveHubVoiceBackend({
VOICE_BACKEND: 'gemini-live',
GEMINI_API_KEY: 'gm',
ELEVENLABS_API_KEY: 'el'
})
expect(backend).toBe('gemini-live')
})
test('falls back to first configured when VOICE_BACKEND unavailable', () => {
const backend = resolveHubVoiceBackend({
VOICE_BACKEND: 'qwen-realtime',
ELEVENLABS_API_KEY: 'el'
})
expect(backend).toBe('elevenlabs')
})
test('returns null when no backends configured', () => {
expect(resolveHubVoiceBackend({})).toBeNull()
})
})
describe('resolveEffectiveVoiceBackend', () => {
const configured = ['elevenlabs', 'gemini-live'] as const
test('prefers stored preference when configured', () => {
expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', 'elevenlabs')).toBe('elevenlabs')
})
test('uses hub default when preference missing or invalid', () => {
expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', null)).toBe('gemini-live')
expect(resolveEffectiveVoiceBackend(configured, 'gemini-live', 'qwen-realtime')).toBe('gemini-live')
})
test('returns null when no backends configured', () => {
expect(resolveEffectiveVoiceBackend([], null, null)).toBeNull()
})
})