108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
fetchPinnedPublicUrl: vi.fn(),
|
|
postFetch: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/lib/server/public-url-fetch', () => ({
|
|
fetchPinnedPublicUrl: mocks.fetchPinnedPublicUrl,
|
|
}));
|
|
|
|
import { generateTTS } from '@/lib/audio/tts-providers';
|
|
|
|
const AUDIO_URL = 'https://dashscope-result.oss-cn-beijing.aliyuncs.com/qwen.wav';
|
|
const CONFIG = {
|
|
providerId: 'qwen-tts' as const,
|
|
apiKey: 'qwen-key',
|
|
voice: 'Cherry',
|
|
};
|
|
|
|
function qwenResponse(audioUrl: unknown = AUDIO_URL): Response {
|
|
return Response.json({ output: { audio: { url: audioUrl } } });
|
|
}
|
|
|
|
function pinned(response: Response) {
|
|
const dispose = vi.fn().mockResolvedValue(undefined);
|
|
mocks.fetchPinnedPublicUrl.mockResolvedValue({ response, dispose });
|
|
return { dispose };
|
|
}
|
|
|
|
describe('Qwen TTS result audio URL download', () => {
|
|
beforeEach(() => {
|
|
mocks.postFetch.mockReset();
|
|
mocks.fetchPinnedPublicUrl.mockReset();
|
|
vi.stubGlobal('fetch', mocks.postFetch);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('downloads through the DNS-pinned public helper and preserves WAV output', async () => {
|
|
mocks.postFetch.mockResolvedValue(qwenResponse());
|
|
const { dispose } = pinned(new Response(new Uint8Array([82, 73, 70, 70])));
|
|
|
|
const result = await generateTTS(CONFIG, '你好');
|
|
|
|
expect(mocks.postFetch).toHaveBeenCalledOnce();
|
|
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(AUDIO_URL, {
|
|
signal: expect.any(AbortSignal),
|
|
maxResponseBytes: 25 * 1024 * 1024,
|
|
bodyTimeoutMs: 30_000,
|
|
});
|
|
expect(Array.from(result.audio)).toEqual([82, 73, 70, 70]);
|
|
expect(result.format).toBe('wav');
|
|
expect(dispose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('fails closed on a redirect and never follows its Location', async () => {
|
|
mocks.postFetch.mockResolvedValue(qwenResponse());
|
|
const { dispose } = pinned(
|
|
new Response(null, {
|
|
status: 302,
|
|
statusText: 'Found',
|
|
headers: { location: 'http://169.254.169.254/latest/meta-data' },
|
|
}),
|
|
);
|
|
|
|
await expect(generateTTS(CONFIG, '你好')).rejects.toThrow(
|
|
'Failed to download audio from URL: Found',
|
|
);
|
|
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledOnce();
|
|
expect(mocks.postFetch).toHaveBeenCalledOnce();
|
|
expect(dispose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('sets a 25 MiB transport cap and propagates an oversized response failure', async () => {
|
|
mocks.postFetch.mockResolvedValue(qwenResponse());
|
|
mocks.fetchPinnedPublicUrl.mockRejectedValue(new Error('response exceeds 25 MiB'));
|
|
|
|
await expect(generateTTS(CONFIG, '你好')).rejects.toThrow('response exceeds 25 MiB');
|
|
expect(mocks.fetchPinnedPublicUrl).toHaveBeenCalledWith(
|
|
AUDIO_URL,
|
|
expect.objectContaining({ maxResponseBytes: 25 * 1024 * 1024 }),
|
|
);
|
|
});
|
|
|
|
it('aborts a stalled result download after 30 seconds', async () => {
|
|
vi.useFakeTimers();
|
|
mocks.postFetch.mockResolvedValue(qwenResponse());
|
|
mocks.fetchPinnedPublicUrl.mockImplementation(
|
|
(_url: string, options: { signal?: AbortSignal }) =>
|
|
new Promise((_resolve, reject) => {
|
|
options.signal?.addEventListener('abort', () => reject(new Error('download aborted')), {
|
|
once: true,
|
|
});
|
|
}),
|
|
);
|
|
|
|
const result = generateTTS(CONFIG, '你好');
|
|
const rejection = expect(result).rejects.toThrow('download aborted');
|
|
await vi.advanceTimersByTimeAsync(30_000);
|
|
|
|
await rejection;
|
|
});
|
|
});
|