ai-chatbot/tests/routes/aliyun-canvas-video.test.ts

176 lines
6.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { test, expect } from "@playwright/test";
import {
buildAliyunCanvasVideoRequest,
executeAliyunCanvasVideo,
} from "@/lib/ai/providers/aliyun-canvas-video";
import { filterCanvasModels } from "@/lib/video-canvas/model-matching";
const input = {
modelId: "wan2.6-i2v-flash",
prompt: "red mug",
mode: "first-frame" as const,
images: ["https://example.com/mug.png"],
duration: 5,
resolution: "720p" as const,
ratio: "adaptive" as const,
apiKey: "test",
baseURL: "https://dashscope.aliyuncs.com/api/v1",
};
test("maps Wan first frame to official synthesis contract, preserving audio false", () => {
expect(buildAliyunCanvasVideoRequest({ ...input, generateAudio: false }, input.modelId)).toEqual({
model: input.modelId,
input: { prompt: "red mug", img_url: input.images[0] },
parameters: {
resolution: "720P",
duration: 5,
audio: false,
prompt_extend: false,
watermark: false,
},
});
});
test("unsupported modes, models and parameter combinations fail before submission", () => {
for (const patch of [
{ mode: "first-last-frame", images: [...input.images, ...input.images] },
{ duration: 1 },
{ duration: 5.5 },
{ resolution: "480p" },
{ audioUrls: ["https://example.com/audio.mp3"] },
{ returnLastFrame: true },
])
expect(() =>
buildAliyunCanvasVideoRequest({ ...input, ...patch } as any, input.modelId),
).toThrow();
expect(() => buildAliyunCanvasVideoRequest(input, "wan3-unknown")).toThrow();
});
test("only the implemented Wan model is selectable, only for first frame", () => {
const model = {
modelId: input.modelId,
providerType: "qianwen-video",
capabilities: { kind: "video", supportedModes: ["first-frame"] },
};
expect(filterCanvasModels([model], "video", "first-frame")).toEqual([model]);
expect(filterCanvasModels([model], "video", "first-last-frame")).toEqual([]);
expect(filterCanvasModels([{ ...model, modelId: "wan3-unknown" }], "video")).toEqual([]);
});
test("polls accepted task without resubmitting, returns upstream artifact and metadata", async () => {
const calls: Array<{ url: string; init: any }> = [];
let polls = 0;
const fetch: typeof globalThis.fetch = async (url, init) => {
calls.push({ url: String(url), init });
return Response.json(
init?.method === "POST"
? { output: { task_id: "task-1" } }
: {
output:
++polls === 1
? { task_status: "RUNNING" }
: { task_status: "SUCCEEDED", video_url: "https://example.com/video.mp4" },
request_id: "req-1",
},
);
};
const result = await executeAliyunCanvasVideo(input, { fetch, sleep: async () => {} });
expect(calls.map((c) => [c.url, c.init.method || "GET"])).toEqual([
[
"https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis",
"POST",
],
["https://dashscope.aliyuncs.com/api/v1/tasks/task-1", "GET"],
["https://dashscope.aliyuncs.com/api/v1/tasks/task-1", "GET"],
]);
expect(result).toMatchObject({
url: "https://example.com/video.mp4",
metadata: { taskId: "task-1", duration: 5, generateAudio: false },
});
});
test("task failure is not returned as a successful video", async () => {
const fetch: typeof globalThis.fetch = async (_url, init) =>
Response.json(
init?.method === "POST"
? { output: { task_id: "task-failed" } }
: { output: { task_status: "FAILED", code: "InvalidParameter" } },
);
await expect(executeAliyunCanvasVideo(input, { fetch, sleep: async () => {} })).rejects.toThrow(
"InvalidParameter,任务 task-failed",
);
});
test("completed task without output is rejected and API auth errors do not expose secrets", async () => {
const missing: typeof globalThis.fetch = async (_url, init) =>
Response.json(
init?.method === "POST"
? { output: { task_id: "empty" } }
: { output: { task_status: "SUCCEEDED" } },
);
await expect(
executeAliyunCanvasVideo(input, { fetch: missing, sleep: async () => {} }),
).rejects.toThrow("没有返回视频");
const denied: typeof globalThis.fetch = async () =>
Response.json(
{ code: "InvalidApiKey", message: "private upstream diagnostic" },
{ status: 401 },
);
await expect(
executeAliyunCanvasVideo(input, { fetch: denied, sleep: async () => {} }),
).rejects.toThrow("阿里云视频请求失败(401):InvalidApiKey");
});
test("resumes a persisted Aliyun task without POST or saving a new task ID", async () => {
const calls: string[] = [];
const result = await executeAliyunCanvasVideo(
{
...input,
lifecycle: {
resume: { taskId: "existing" },
accepted: async () => {
throw Error("unexpected create");
},
assertActive: async () => {},
},
},
{
sleep: async () => {},
fetch: async (url, init) => {
calls.push(init?.method || "GET");
expect(String(url)).toContain("/tasks/existing");
return Response.json({
output: { task_status: "SUCCEEDED", video_url: "https://example.com/old.mp4" },
});
},
},
);
expect(calls).toEqual(["GET"]);
expect(result.url).toContain("old.mp4");
});
test("saves accepted video ID before first GET and stops when execution ownership changes", async () => {
let saved = false;
let gets = 0;
await expect(
executeAliyunCanvasVideo(
{
...input,
lifecycle: {
accepted: async ({ taskId }) => {
expect(taskId).toBe("accepted");
saved = true;
},
assertActive: async () => {
if (saved) throw Error("lease lost");
},
},
},
{
sleep: async () => {},
fetch: async (_url, init) => {
if (init?.method !== "POST") {
gets++;
expect(saved).toBe(true);
}
return Response.json({ output: { task_id: "accepted" } });
},
},
),
).rejects.toThrow("lease lost");
expect(saved).toBe(true);
expect(gets).toBe(0);
});