Files
openmaic/OpenMAIC/tests/media/video-polled-adapters.test.ts
2026-08-16 14:58:47 +08:00

485 lines
15 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { generateWithGrokVideo } from '@/lib/media/adapters/grok-video-adapter';
import { generateWithHappyHorse } from '@/lib/media/adapters/happyhorse-adapter';
import { generateWithKling } from '@/lib/media/adapters/kling-adapter';
import { generateWithMiniMaxVideo } from '@/lib/media/adapters/minimax-video-adapter';
import { generateWithSeedance } from '@/lib/media/adapters/seedance-adapter';
import { generateWithVeo } from '@/lib/media/adapters/veo-adapter';
const fetchMock = vi.fn();
function jsonResponse(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
describe('polled video adapter compatibility', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
fetchMock.mockReset();
vi.unstubAllGlobals();
vi.useRealTimers();
});
it('keeps Seedance task routing and success extraction unchanged', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse({ id: 'seed-task' })).mockResolvedValueOnce(
jsonResponse({
id: 'seed-task',
model: 'doubao-seedance-2-0-260128',
status: 'succeeded',
content: { video_url: 'https://cdn.example.com/seed.mp4' },
resolution: '720p',
ratio: '16:9',
duration: 5,
}),
);
const promise = generateWithSeedance(
{ providerId: 'seedance', apiKey: 'seed-key' },
{ prompt: 'a paper city', aspectRatio: '16:9', resolution: '720p', duration: 5 },
);
await vi.advanceTimersByTimeAsync(4_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toEqual({
url: 'https://cdn.example.com/seed.mp4',
duration: 5,
width: 1280,
height: 720,
});
expect(fetchMock.mock.calls[1][0]).toBe(
'https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/seed-task',
);
});
it('preserves the Seedance timeout message and exact poll count', async () => {
fetchMock.mockResolvedValueOnce(jsonResponse({ id: 'seed-timeout' })).mockImplementation(() =>
Promise.resolve(
jsonResponse({
id: 'seed-timeout',
model: 'doubao-seedance-2-0-260128',
status: 'running',
}),
),
);
const promise = generateWithSeedance(
{ providerId: 'seedance', apiKey: 'seed-key' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Seedance video generation timed out after 300s (task: seed-timeout)',
);
await vi.advanceTimersByTimeAsync(300_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(61);
});
it('keeps Kling task routing and success extraction unchanged', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({
code: 0,
message: 'success',
data: { task_id: 'kling-task', task_status: 'submitted' },
}),
)
.mockResolvedValueOnce(
jsonResponse({
code: 0,
message: 'success',
data: {
task_id: 'kling-task',
task_status: 'succeed',
task_result: {
videos: [
{
id: 'video-1',
url: 'https://cdn.example.com/kling.mp4',
duration: '5',
},
],
},
},
}),
);
const promise = generateWithKling(
{ providerId: 'kling', apiKey: 'access:secret', baseUrl: 'https://kling.example' },
{ prompt: 'a paper city', aspectRatio: '16:9', duration: 5 },
);
await vi.advanceTimersByTimeAsync(4_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toEqual({
url: 'https://cdn.example.com/kling.mp4',
duration: 5,
width: 1280,
height: 720,
});
expect(fetchMock.mock.calls[1][0]).toBe(
'https://kling.example/v1/videos/text2video/kling-task',
);
});
it('preserves the Kling timeout message and exact poll count', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({
code: 0,
message: 'success',
data: { task_id: 'kling-timeout', task_status: 'submitted' },
}),
)
.mockImplementation(() =>
Promise.resolve(
jsonResponse({
code: 0,
message: 'success',
data: { task_id: 'kling-timeout', task_status: 'processing' },
}),
),
);
const promise = generateWithKling(
{ providerId: 'kling', apiKey: 'access:secret', baseUrl: 'https://kling.example' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Kling video generation timed out after 600s (task: kling-timeout)',
);
await vi.advanceTimersByTimeAsync(600_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(121);
});
it('preserves the Kling terminal failure message', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({
code: 0,
message: 'success',
data: { task_id: 'kling-failed', task_status: 'submitted' },
}),
)
.mockResolvedValueOnce(
jsonResponse({
code: 0,
message: 'success',
data: {
task_id: 'kling-failed',
task_status: 'failed',
task_status_msg: 'content rejected',
},
}),
);
const promise = generateWithKling(
{ providerId: 'kling', apiKey: 'access:secret', baseUrl: 'https://kling.example' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Kling video generation failed: content rejected',
);
await vi.advanceTimersByTimeAsync(5_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('keeps Grok task routing and success extraction unchanged', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ request_id: 'grok-request' }))
.mockResolvedValueOnce(
jsonResponse({
status: 'done',
video: { url: 'https://cdn.example.com/grok.mp4', duration: 6 },
model: 'grok-imagine-video',
}),
);
const promise = generateWithGrokVideo(
{ providerId: 'grok-video', apiKey: 'grok-key', baseUrl: 'https://grok.example/v1' },
{ prompt: 'a paper city', aspectRatio: '16:9', duration: 6 },
);
await vi.advanceTimersByTimeAsync(9_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toEqual({
url: 'https://cdn.example.com/grok.mp4',
duration: 6,
width: 1280,
height: 720,
});
expect(fetchMock.mock.calls[1][0]).toBe('https://grok.example/v1/videos/grok-request');
});
it('preserves the Grok timeout message and exact poll count', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ request_id: 'grok-timeout' }))
.mockImplementation(() => Promise.resolve(jsonResponse({ status: 'pending', progress: 50 })));
const promise = generateWithGrokVideo(
{ providerId: 'grok-video', apiKey: 'grok-key', baseUrl: 'https://grok.example/v1' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Grok video generation timed out after 600s (request: grok-timeout)',
);
await vi.advanceTimersByTimeAsync(600_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(61);
});
it('preserves the Grok terminal failure message', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ request_id: 'grok-failed' }))
.mockResolvedValueOnce(
jsonResponse({ status: 'failed', progress: 42, model: 'grok-imagine-video' }),
);
const promise = generateWithGrokVideo(
{ providerId: 'grok-video', apiKey: 'grok-key', baseUrl: 'https://grok.example/v1' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Grok video generation failed: {"status":"failed","progress":42,"model":"grok-imagine-video"}',
);
await vi.advanceTimersByTimeAsync(10_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('keeps HappyHorse task routing and success extraction unchanged', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({
output: { task_id: 'horse-task', task_status: 'PENDING' },
request_id: 'request-1',
}),
)
.mockResolvedValueOnce(
jsonResponse({
output: {
task_id: 'horse-task',
task_status: 'SUCCEEDED',
video_url: 'https://cdn.example.com/horse.mp4',
},
usage: { duration: 5, SR: 720, ratio: '16:9' },
}),
);
const promise = generateWithHappyHorse(
{ providerId: 'happyhorse', apiKey: 'horse-key' },
{ prompt: 'a paper city', aspectRatio: '16:9', resolution: '720p', duration: 5 },
);
await vi.advanceTimersByTimeAsync(14_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toEqual({
url: 'https://cdn.example.com/horse.mp4',
duration: 5,
width: 1280,
height: 720,
});
expect(fetchMock.mock.calls[1][0]).toBe(
'https://dashscope.aliyuncs.com/api/v1/tasks/horse-task',
);
});
it('preserves the HappyHorse timeout message and exact poll count', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({ output: { task_id: 'horse-timeout', task_status: 'PENDING' } }),
)
.mockImplementation(() =>
Promise.resolve(
jsonResponse({ output: { task_id: 'horse-timeout', task_status: 'RUNNING' } }),
),
);
const promise = generateWithHappyHorse(
{ providerId: 'happyhorse', apiKey: 'horse-key' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'HappyHorse video generation timed out after 600s (task: horse-timeout)',
);
await vi.advanceTimersByTimeAsync(600_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(41);
});
it('returns an immediately completed Veo operation without waiting or polling', async () => {
fetchMock.mockResolvedValueOnce(
jsonResponse({
name: 'operations/veo-immediate',
done: true,
response: {
videos: [{ bytesBase64Encoded: 'dmlkZW8=', mimeType: 'video/mp4' }],
},
}),
);
const result = await generateWithVeo(
{ providerId: 'veo', apiKey: 'veo-key', baseUrl: 'https://veo.example' },
{ prompt: 'a paper city', aspectRatio: '16:9', duration: 8 },
);
expect(result).toEqual({
url: 'data:video/mp4;base64,dmlkZW8=',
duration: 8,
width: 1280,
height: 720,
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0);
});
it('rejects an immediately failed Veo operation without waiting or polling', async () => {
fetchMock.mockResolvedValueOnce(
jsonResponse({
name: 'operations/veo-failed',
done: true,
error: { code: 13, message: 'render failed', status: 'INTERNAL' },
}),
);
await expect(
generateWithVeo(
{ providerId: 'veo', apiKey: 'veo-key', baseUrl: 'https://veo.example' },
{ prompt: 'a paper city' },
),
).rejects.toThrow('Veo generation failed: 13 - render failed');
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(vi.getTimerCount()).toBe(0);
});
it('keeps Veo operation routing and polled success extraction unchanged', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ name: 'operations/veo-task' }))
.mockResolvedValueOnce(
jsonResponse({
name: 'operations/veo-task',
done: true,
response: {
videos: [{ bytesBase64Encoded: 'cG9sbGVk', mimeType: 'video/webm' }],
},
}),
);
const promise = generateWithVeo(
{ providerId: 'veo', apiKey: 'veo-key', baseUrl: 'https://veo.example' },
{ prompt: 'a paper city', aspectRatio: '9:16', duration: 8 },
);
await vi.advanceTimersByTimeAsync(9_999);
expect(fetchMock).toHaveBeenCalledTimes(1);
await vi.advanceTimersByTimeAsync(1);
await expect(promise).resolves.toEqual({
url: 'data:video/webm;base64,cG9sbGVk',
duration: 8,
width: 720,
height: 1280,
});
expect(JSON.parse(fetchMock.mock.calls[1][1].body as string)).toEqual({
operationName: 'operations/veo-task',
});
});
it('preserves the Veo timeout message and exact poll count', async () => {
fetchMock
.mockResolvedValueOnce(jsonResponse({ name: 'operations/veo-timeout' }))
.mockImplementation(() =>
Promise.resolve(jsonResponse({ name: 'operations/veo-timeout', done: false })),
);
const promise = generateWithVeo(
{ providerId: 'veo', apiKey: 'veo-key', baseUrl: 'https://veo.example' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'Veo video generation timed out after 10 minutes',
);
await vi.advanceTimersByTimeAsync(600_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(61);
});
it('preserves the MiniMax terminal failure message', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({ task_id: 'minimax-failed', base_resp: { status_code: 0, status_msg: '' } }),
)
.mockResolvedValueOnce(
jsonResponse({
task_id: 'minimax-failed',
status: 'Fail',
base_resp: { status_code: 1008, status_msg: 'quota exceeded' },
}),
);
const promise = generateWithMiniMaxVideo(
{ providerId: 'minimax-video', apiKey: 'minimax-key' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'MiniMax Video generation failed: quota exceeded',
);
await vi.advanceTimersByTimeAsync(5_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('preserves the MiniMax timeout message with the last status and exact poll count', async () => {
fetchMock
.mockResolvedValueOnce(
jsonResponse({ task_id: 'minimax-timeout', base_resp: { status_code: 0, status_msg: '' } }),
)
.mockImplementation(() =>
Promise.resolve(
jsonResponse({
task_id: 'minimax-timeout',
status: 'Processing',
base_resp: { status_code: 0, status_msg: '' },
}),
),
);
const promise = generateWithMiniMaxVideo(
{ providerId: 'minimax-video', apiKey: 'minimax-key' },
{ prompt: 'a paper city' },
);
const rejection = expect(promise).rejects.toThrow(
'MiniMax Video: timeout after 120 polls, last status: Processing',
);
await vi.advanceTimersByTimeAsync(600_000);
await rejection;
expect(fetchMock).toHaveBeenCalledTimes(121);
});
});