项目文件夹

文件
wehub-resource-sync c48612c494
CI / E2E Tests (push) Has been cancelled
CI / Lint, Typecheck & Unit Tests (push) Has been cancelled
Docs Build / Build docs site (push) Has been cancelled
Publish @openmaic packages / Build, validate & publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:23 +08:00

141 行
5.6 KiB
TypeScript

/**
* Tests for voice-resolver under the unified enablement model (#665):
* - getEnabledProvidersWithVoices includes only enabled providers and never
* browser-native;
* - resolveAgentVoice is deterministic among enabled providers, honors a still
* -enabled voiceConfig, never auto-assigns browser-native, and returns null
* when nothing is enabled (no hardcoded fallback).
*/
import { describe, it, expect } from 'vitest';
import type { AgentConfig } from '@/lib/orchestration/registry/types';
import {
getEnabledProvidersWithVoices,
getSelectableProvidersWithVoices,
resolveAgentVoice,
type ProviderWithVoices,
} from '@/lib/audio/voice-resolver';
const agent = (voiceConfig?: AgentConfig['voiceConfig']) => ({ voiceConfig }) as AgentConfig;
describe('getEnabledProvidersWithVoices', () => {
it('includes configured+enabled providers, excludes disabled/unconfigured/browser-native', () => {
const providers = getEnabledProvidersWithVoices({
'openai-tts': { apiKey: 'k', enabled: true },
'qwen-tts': { apiKey: 'k', enabled: false }, // user-disabled
'glm-tts': { apiKey: 'k', serverDisabled: true }, // server-disabled
'lemonade-tts': {}, // not configured (defaultBaseUrl dropped)
'browser-native-tts': { enabled: true }, // never in this list
});
const ids = providers.map((p) => p.providerId);
expect(ids).toContain('openai-tts');
expect(ids).not.toContain('qwen-tts');
expect(ids).not.toContain('glm-tts');
expect(ids).not.toContain('lemonade-tts');
expect(ids).not.toContain('browser-native-tts');
});
});
describe('resolveAgentVoice', () => {
const openai: ProviderWithVoices = {
providerId: 'openai-tts',
providerName: 'OpenAI TTS',
voices: [
{ id: 'alloy', name: 'Alloy' },
{ id: 'echo', name: 'Echo' },
],
modelGroups: [],
};
it('returns null when nothing is enabled (no silent browser-native fallback)', () => {
expect(resolveAgentVoice(agent(), 0, [])).toBeNull();
});
it('deterministically picks among enabled providers by agent index', () => {
expect(resolveAgentVoice(agent(), 0, [openai])).toEqual({
providerId: 'openai-tts',
voiceId: 'alloy',
});
expect(resolveAgentVoice(agent(), 1, [openai])).toEqual({
providerId: 'openai-tts',
voiceId: 'echo',
});
// index wraps deterministically
expect(resolveAgentVoice(agent(), 2, [openai])?.voiceId).toBe('alloy');
});
it('honors a voiceConfig only when its provider is still enabled', () => {
const cfg = { providerId: 'openai-tts' as const, voiceId: 'echo' };
expect(resolveAgentVoice(agent(cfg), 0, [openai])).toEqual({
providerId: 'openai-tts',
modelId: undefined,
voiceId: 'echo',
});
// provider not in enabled list ⇒ fall back to deterministic pick (not the
// stale voiceConfig)
expect(resolveAgentVoice(agent(cfg), 0, [])).toBeNull();
});
it('never auto-assigns browser-native, but honors it as an explicit choice', () => {
const bn = { providerId: 'browser-native-tts' as const, voiceId: 'default' };
// browser-native not in the enabled list ⇒ explicit config ignored, no fallback
expect(resolveAgentVoice(agent(bn), 0, [openai])).toEqual({
providerId: 'openai-tts',
voiceId: 'alloy',
});
// browser-native present (user enabled it) ⇒ explicit choice honored
const withBn: ProviderWithVoices = {
providerId: 'browser-native-tts',
providerName: 'Browser Native',
voices: [{ id: 'default', name: 'Default' }],
modelGroups: [],
};
expect(resolveAgentVoice(agent(bn), 0, [openai, withBn])).toEqual({
providerId: 'browser-native-tts',
modelId: undefined,
voiceId: 'default',
});
});
});
describe('getSelectableProvidersWithVoices (unified picker + discussion source)', () => {
const cfg = {
'qwen-tts': { apiKey: 'k', enabled: true },
'browser-native-tts': { enabled: true },
};
const bn = [{ voiceURI: 'os-zh', name: 'OS Chinese' }];
it('= enabled providers + browser-native when enabled AND browser exposes voices', () => {
const ids = getSelectableProvidersWithVoices(cfg, [], bn).map((p) => p.providerId);
expect(ids).toContain('qwen-tts');
expect(ids).toContain('browser-native-tts');
});
it('omits browser-native when the browser exposes no voices (e.g. headless)', () => {
const ids = getSelectableProvidersWithVoices(cfg, [], []).map((p) => p.providerId);
expect(ids).toContain('qwen-tts');
expect(ids).not.toContain('browser-native-tts');
});
it('omits browser-native when the user has not enabled it', () => {
const ids = getSelectableProvidersWithVoices(
{ 'qwen-tts': { apiKey: 'k', enabled: true }, 'browser-native-tts': { enabled: false } },
[],
bn,
).map((p) => p.providerId);
expect(ids).not.toContain('browser-native-tts');
});
it('lets the teacher path (global voice as synthetic voiceConfig) and a student resolve through the SAME list — both audible when a provider is selectable', () => {
const providers = getSelectableProvidersWithVoices(cfg, [], bn);
// teacher: global selection = browser-native (only client-dynamic provider)
const teacher = {
role: 'teacher',
voiceConfig: { providerId: 'browser-native-tts' as const, voiceId: 'os-zh' },
} as AgentConfig;
expect(resolveAgentVoice(teacher, 0, providers)?.providerId).toBe('browser-native-tts');
// student with no voiceConfig falls back deterministically to an enabled provider — not null
const student = {} as AgentConfig;
expect(resolveAgentVoice(student, 0, providers)).not.toBeNull();
});
});