117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest';
|
|
import { generateTTS } from '@/lib/audio/tts-providers';
|
|
|
|
const mockFetch = vi.fn() as Mock;
|
|
vi.stubGlobal('fetch', mockFetch);
|
|
|
|
// Build the chunked NDJSON body the Doubao /unidirectional endpoint streams:
|
|
// one {code,data} JSON object per line, ending with the 20000000 sentinel.
|
|
function ndjsonBody(audioChunksB64: string[]): string {
|
|
const lines = audioChunksB64.map((d) => JSON.stringify({ code: 0, data: d }));
|
|
lines.push(JSON.stringify({ code: 20000000, message: 'done' }));
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function okResponse(body: string) {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
text: async () => body,
|
|
headers: { get: () => 'application/json' },
|
|
};
|
|
}
|
|
|
|
const helloB64 = Buffer.from([1, 2, 3, 4]).toString('base64');
|
|
|
|
describe('Doubao TTS dual auth', () => {
|
|
beforeEach(() => {
|
|
mockFetch.mockReset();
|
|
});
|
|
|
|
it('uses X-Api-Key + plan endpoint for an Agent Plan single key (no colon)', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(ndjsonBody([helloB64])));
|
|
|
|
const result = await generateTTS(
|
|
{
|
|
providerId: 'doubao-tts',
|
|
apiKey: 'ark-plan-key-123',
|
|
baseUrl: 'https://openspeech.bytedance.com/api/plan/tts',
|
|
voice: 'zh_female_vv_uranus_bigtts',
|
|
},
|
|
'你好',
|
|
);
|
|
|
|
const [url, init] = mockFetch.mock.calls[0];
|
|
expect(url).toBe('https://openspeech.bytedance.com/api/plan/tts/unidirectional');
|
|
expect(init.headers['X-Api-Key']).toBe('ark-plan-key-123');
|
|
expect(init.headers['X-Api-Resource-Id']).toBe('seed-tts-2.0');
|
|
// Plan mode must NOT send the appId/accessKey pair headers.
|
|
expect(init.headers['X-Api-App-Id']).toBeUndefined();
|
|
expect(init.headers['X-Api-Access-Key']).toBeUndefined();
|
|
expect(result.audio).toBeInstanceOf(Uint8Array);
|
|
expect(result.audio.byteLength).toBe(4);
|
|
});
|
|
|
|
it('uses X-Api-App-Id + X-Api-Access-Key for a classic appId:accessKey key', async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(ndjsonBody([helloB64])));
|
|
|
|
await generateTTS(
|
|
{
|
|
providerId: 'doubao-tts',
|
|
apiKey: 'app123:secretAccessKey',
|
|
voice: 'zh_female_vv_uranus_bigtts',
|
|
},
|
|
'你好',
|
|
);
|
|
|
|
const init = mockFetch.mock.calls[0][1];
|
|
expect(init.headers['X-Api-App-Id']).toBe('app123');
|
|
expect(init.headers['X-Api-Access-Key']).toBe('secretAccessKey');
|
|
expect(init.headers['X-Api-Key']).toBeUndefined();
|
|
});
|
|
|
|
it('rejects an empty key before making a request', async () => {
|
|
// The generic generateTTS guard (requiresApiKey && !apiKey) fires first.
|
|
await expect(
|
|
generateTTS({ providerId: 'doubao-tts', apiKey: '', voice: 'x' }, 'hi'),
|
|
).rejects.toThrow(/API key required for TTS provider: doubao-tts/);
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects a malformed appId:accessKey (empty half) without a request', async () => {
|
|
await expect(
|
|
generateTTS({ providerId: 'doubao-tts', apiKey: 'appId:', voice: 'x' }, 'hi'),
|
|
).rejects.toThrow(/appId:accessKey is malformed/);
|
|
await expect(
|
|
generateTTS({ providerId: 'doubao-tts', apiKey: ':secret', voice: 'x' }, 'hi'),
|
|
).rejects.toThrow(/appId:accessKey is malformed/);
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('aggregates base64 audio chunks across the NDJSON stream', async () => {
|
|
const a = Buffer.from([1, 2]).toString('base64');
|
|
const b = Buffer.from([3, 4, 5]).toString('base64');
|
|
mockFetch.mockResolvedValueOnce(okResponse(ndjsonBody([a, b])));
|
|
|
|
const result = await generateTTS(
|
|
{ providerId: 'doubao-tts', apiKey: 'ark-k', voice: 'v' },
|
|
'hi',
|
|
);
|
|
|
|
expect(Array.from(result.audio)).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
it('parses concatenated JSON errors whose message contains braces', async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
okResponse(
|
|
JSON.stringify({ code: 0, data: helloB64 }) +
|
|
JSON.stringify({ code: 40000001, message: 'bad payload: {"field":"text"}' }),
|
|
),
|
|
);
|
|
|
|
await expect(
|
|
generateTTS({ providerId: 'doubao-tts', apiKey: 'ark-k', voice: 'v' }, 'hi'),
|
|
).rejects.toThrow(/bad payload: \{"field":"text"\}/);
|
|
});
|
|
});
|