Files
openmaic/OpenMAIC/tests/audio/qwen-tts-url-fetch.test.ts
T

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