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