thu-maic--openmaic
141 行
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|