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; }); });