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

750 lines
26 KiB
TypeScript

import {
executeImageOperation,
imageOperationSchema,
ImageOperationUncertainError,
} from "@/lib/video-canvas/image-operation";
import {
mergeStoryTaskResult,
requireStoryPromptResult,
} from "@/lib/video-canvas/story-task-attachment";
import { test, expect } from "@playwright/test";
import {
canvasTaskRequestSchema,
reconcileCanvasTasks,
requireAnalysisResult,
canCancelCanvasTask,
type CanvasGenerationTask,
} from "@/lib/video-canvas/task-contract";
import { chooseStoryVideoSettings } from "@/lib/video-canvas/model-matching";
type TaskNode = { id: string; draftValues?: Record<string, any>; [key: string]: any };
const node: TaskNode = {
id: "n1",
title: "镜头",
kind: "media",
inputType: "image",
x: 0,
y: 0,
width: 300,
height: 200,
draftValues: { rows: [{ sceneDescription: "旧内容" }] },
};
const request = {
requestId: "11111111-1111-4111-8111-111111111111",
projectId: "22222222-2222-4222-8222-222222222222",
kind: "image" as const,
node,
params: { prompt: "cup" },
edges: [],
};
const task: CanvasGenerationTask = {
id: "task1",
requestId: request.requestId,
status: "succeeded",
createdAt: "2026-09-17T01:01:00Z",
request: request as any,
result: {
images: [
{ originalUrl: "https://example.com/1.png" },
{ originalUrl: "https://example.com/2.png" },
],
},
};
test("restores accepted tasks missing from a pre-submit canvas snapshot, including every image", () => {
const result = reconcileCanvasTasks<TaskNode>([], [task], "2026-09-17T01:00:00Z");
expect(result.map((n) => n.id)).toEqual(["n1", "n1-result-task1-1"]);
expect(result[0].draftValues?.url).toHaveLength(2);
expect(reconcileCanvasTasks<TaskNode>(result, [task], "2026-09-17T01:00:00Z")).toBe(result);
});
test("does not resurrect nodes removed after task creation or hidden on the server", () => {
expect(reconcileCanvasTasks<TaskNode>([], [task], "2026-09-17T01:02:00Z")).toEqual([]);
expect(
reconcileCanvasTasks<TaskNode>(
[],
[{ ...task, request: { ...request, node: node as any, hiddenNodeIds: ["n1"] } }],
"2026-09-17T01:00:00Z",
),
).toEqual([]);
});
test("late result from an older request cannot overwrite a new generation on the same node", () => {
const current = {
...node,
draftValues: { taskRequestId: "new-request", mediaUrl: "new-result" },
};
expect(reconcileCanvasTasks<TaskNode>([current], [task], "2026-09-17T01:00:00Z")[0]).toBe(
current,
);
});
test("task failure retains the previous script instead of marking an empty result successful", () => {
const result = reconcileCanvasTasks<TaskNode>(
[node],
[{ ...task, status: "failed", error: "模型错误" }],
"2026-09-17T01:00:00Z",
)[0];
expect(result.draftValues).toMatchObject({
rows: [{ sceneDescription: "旧内容" }],
generationStatus: "failed",
generationError: "模型错误",
});
});
test("queue and running states show actual state without invented progress", () => {
const pending = reconcileCanvasTasks<TaskNode>(
[node],
[{ ...task, status: "pending" }],
"2026-09-17T01:00:00Z",
);
expect(pending[0].draftValues).toMatchObject({ generationProgress: 0, pendingLabel: "排队中" });
expect(
reconcileCanvasTasks<TaskNode>(
pending,
[{ ...task, status: "running" }],
"2026-09-17T01:00:00Z",
)[0].draftValues?.pendingLabel,
).toBe("生成中");
});
test("interrupted tasks accept a late real result while cancelled queued tasks stay visibly cancelled", () => {
const pending = reconcileCanvasTasks<TaskNode>(
[node],
[{ ...task, status: "interrupted", error: "待核实" }],
"2026-09-17T01:00:00Z",
);
expect(
reconcileCanvasTasks<TaskNode>(pending, [task], "2026-09-17T01:00:00Z")[0].draftValues
?.generationStatus,
).toBe("complete");
expect(
reconcileCanvasTasks<TaskNode>(
[node],
[{ ...task, status: "cancelled" }],
"2026-09-17T01:00:00Z",
)[0].subtitle,
).toBe("已取消");
expect(["pending", "running", "succeeded", "interrupted"].map(canCancelCanvasTask)).toEqual([
true,
false,
false,
false,
]);
});
test("analysis rejects success envelopes without meaningful output or script rows", () => {
for (const data of [
undefined,
{},
{ analysisRows: [] },
{ analysisRows: [{}], raw: "some summary" },
])
expect(() => requireAnalysisResult(data, "story-script")).toThrow();
expect(() => requireAnalysisResult({ raw: " " }, "agent-plan")).toThrow();
expect(
requireAnalysisResult({ analysisRows: [{ sceneDescription: "杯柄特写" }] }, "story-script"),
).toBeTruthy();
});
test("task snapshots require stable project/request identities and bounded geometry", () => {
expect(canvasTaskRequestSchema.safeParse(request).success).toBe(true);
for (const patch of [
{ requestId: "bad" },
{ projectId: "bad" },
{ node: { ...node, width: Infinity } },
{ node: { ...node, id: "" } },
])
expect(canvasTaskRequestSchema.safeParse({ ...request, ...patch }).success).toBe(false);
});
const model = {
id: "video-model",
modelId: "wan2.6-i2v-flash",
providerType: "qianwen-video",
capabilities: {
kind: "video",
supportedModes: ["first-frame"],
durationOptions: [5, 10],
supportedRatios: ["adaptive"],
supportedResolutions: ["720p"],
supportGenerateAudio: true,
},
};
test("story batches match the actual input mode and discrete provider parameters", () => {
expect(chooseStoryVideoSettings([model], true, 7)).toMatchObject({
mode: "first-frame",
duration: 5,
ratio: "adaptive",
resolution: "720p",
generateAudio: true,
});
expect(() => chooseStoryVideoSettings([model], false, 5)).toThrow("文生视频");
const text = {
...model,
id: "text-model",
providerType: "vip98k",
modelId: "veo",
capabilities: { ...model.capabilities, supportedModes: ["text-to-video"] },
};
expect(chooseStoryVideoSettings([model, text], false, 9, model.id)).toMatchObject({
mode: "text-to-video",
model: "text-model",
duration: 10,
});
});
test("unknown model specifications cannot silently invent 720p or five seconds", () => {
expect(() =>
chooseStoryVideoSettings(
[{ ...model, capabilities: { kind: "video", supportedModes: ["first-frame"] } }],
true,
5,
),
).toThrow("规格");
});
test("refresh recovers the newest accepted request when the saved node still points to an older result", () => {
const newer = {
...task,
id: "task2",
requestId: "new-request",
createdAt: "2026-09-17T01:03:00Z",
result: { images: [{ originalUrl: "https://example.com/new.png" }] },
};
const old = { ...node, draftValues: { taskRequestId: task.requestId, mediaUrl: "old" } };
const result = reconcileCanvasTasks<TaskNode>([old], [task, newer], "2026-09-17T01:02:00Z");
expect(result[0].draftValues?.mediaUrl).toBe("https://example.com/new.png");
expect(result[0].draftValues?.pendingLabel).toBe("");
expect(
reconcileCanvasTasks<TaskNode>([], [task, newer], "2026-09-17T01:00:00Z")[0].draftValues
?.taskRequestId,
).toBe("new-request");
});
test("grid results preserve the grid action contract", () => {
const grid = { ...node, draftValues: { action: "IMAGE_GENERATE_NINE_GRID" } };
expect(
reconcileCanvasTasks<TaskNode>([grid], [task], "2026-09-17T01:00:00Z")[0].draftValues?.action,
).toBe("IMAGE_GENERATE_NINE_GRID");
});
test("a rerun retains distinct extra results without reusing the previous request's image", () => {
const first = reconcileCanvasTasks<TaskNode>([node], [task], "2026-09-17T01:00:00Z");
const newer = {
...task,
id: "task2",
requestId: "next",
createdAt: "2026-09-17T01:03:00Z",
result: { images: [{ originalUrl: "new1" }, { originalUrl: "new2" }] },
};
const next = reconcileCanvasTasks<TaskNode>(first, [task, newer], "2026-09-17T01:02:00Z");
expect(next.find((n) => n.id === "n1-result-task2-1")?.draftValues?.mediaUrl).toBe("new2");
expect(reconcileCanvasTasks<TaskNode>(next, [task, newer], "2026-09-17T01:02:00Z")).toBe(next);
});
test("image postprocessing validates provider parameters before dispatch", () => {
expect(
imageOperationSchema.safeParse({
operation: "angle-edit",
imageUrl: "https://example.com/a.png",
horizontalAngle: 180,
verticalAngle: -30,
zoom: 5,
}).success,
).toBe(true);
for (const value of [
{ operation: "shell", imageUrl: "https://example.com/a.png" },
{ operation: "enhance", imageUrl: "file:///etc/passwd" },
{ operation: "angle-edit", imageUrl: "https://example.com/a.png" },
{ operation: "enhance", imageUrl: "https://example.com/a.png", scaleFactor: 100 },
])
expect(imageOperationSchema.safeParse(value).success).toBe(false);
});
test("an accepted enhance task is polled without repeating POST and retains its upstream identity", async () => {
let submits = 0,
polls = 0;
const checkpoints: any[] = [];
const handler = async () =>
Response.json({ success: true, original_url: "https://example.com/result.png" });
const result = await executeImageOperation(
{ operation: "enhance", imageUrl: "https://example.com/source.png", scaleFactor: 4 },
"owner",
{
enhance: async (req, user) => {
submits++;
expect(user).toBe("owner");
expect(await req.json()).toMatchObject({ scaleFactor: 4 });
return Response.json(
{ success: true, processing: true, taskId: "upstream-1" },
{ status: 202 },
);
},
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance: async (req, user) => {
polls++;
expect(user).toBe("owner");
expect(new URL(req.url).searchParams.get("taskId")).toBe("upstream-1");
return polls < 2 ? Response.json({ processing: true }, { status: 202 }) : handler();
},
},
{
wait: async () => {},
checkpoint: async (value) => {
checkpoints.push(value);
},
},
);
expect(submits).toBe(1);
expect(polls).toBe(2);
expect(checkpoints[0].upstreamTask.taskId).toBe("upstream-1");
expect(result.images[0].originalUrl).toBe("https://example.com/result.png");
});
test("a pending or disconnected upstream query is reported as uncertain rather than submitted again", async () => {
const handler = async () =>
Response.json({ processing: true, taskId: "upstream-1" }, { status: 202 });
for (const pollEnhance of [
handler,
async () => {
throw Error("connection lost");
},
async () => Response.json({ error: "upstream temporarily unavailable" }, { status: 502 }),
]) {
await expect(
executeImageOperation(
{ operation: "enhance", imageUrl: "https://example.com/source.png" },
"owner",
{
enhance: handler,
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance,
},
{ wait: async () => {}, maxPolls: 2 },
),
).rejects.toBeInstanceOf(ImageOperationUncertainError);
}
});
test("angle operation sends normalized numeric fields and accepts the dedicated response shape", async () => {
const handler = async (req: Request) => {
expect(await req.json()).toEqual({
image_url: "https://example.com/source.png",
horizontal_angle: 180,
vertical_angle: 45,
zoom: 5,
});
return Response.json({ success: true, image_url: "https://example.com/angle.png" });
};
const result = await executeImageOperation(
{
operation: "angle-edit",
imageUrl: "https://example.com/source.png",
horizontalAngle: 180,
verticalAngle: 45,
zoom: 5,
},
"owner",
{
enhance: handler,
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance: handler,
},
);
expect(result.images[0].originalUrl).toContain("angle.png");
expect(
reconcileCanvasTasks<TaskNode>(
[node],
[{ ...task, request: { ...task.request, kind: "image-operation" }, result }],
task.createdAt,
)[0].inputType,
).toBe("image");
});
test("postprocessing preserves upstream failures and rejects an empty success", async () => {
for (const payload of [{ success: false, error: "积分不足" }, { success: true }]) {
const handler = async () => Response.json(payload);
await expect(
executeImageOperation(
{ operation: "remove-bg", imageUrl: "https://example.com/a.png" },
"owner",
{
enhance: handler,
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance: handler,
},
),
).rejects.toThrow();
}
});
const asset = { id: "asset-1", name: "杯子", description: "白色", type: "prop", imageUrl: "" };
const attachment = { type: "story-asset" as const, nodeId: "script", asset };
test("asset results merge by stable identity after reordering and are only applied once", () => {
const draft = { storyAssets: [{ ...asset, id: "other" }, asset] };
const result = mergeStoryTaskResult(draft, attachment, task.result, "job");
expect(result.storyAssets[1].imageUrl).toBe("https://example.com/1.png");
expect(result.storyAssets[0].imageUrl).toBe("");
expect(mergeStoryTaskResult(result, attachment, task.result, "job")).toBe(result);
});
test("late asset results preserve user edits and deletions", () => {
for (const storyAssets of [
[{ ...asset, imageUrl: "user-upload" }],
[{ ...asset, description: "changed" }],
[],
]) {
const result = mergeStoryTaskResult({ storyAssets }, attachment, task.result, "job");
expect(result.storyAssets).toEqual(storyAssets);
expect(result.storyTaskResults.job.status).toBe("conflict");
}
});
test("prompt results merge only their original rows without replacing dialogue or edited rows", () => {
const rows = [
{ id: "r1", dialogue: "原对白", imagePrompt: "", motionPrompt: "" },
{ id: "r2", dialogue: "第二行", imagePrompt: "", motionPrompt: "" },
];
const target = { type: "story-prompts" as const, nodeId: "script", rows };
const data = {
analysisRows: [
{ imagePrompt: "image 1", motionPrompt: "move 1" },
{ imagePrompt: "image 2", motionPrompt: "move 2" },
],
};
const current = [{ ...rows[1], dialogue: "用户修改" }, rows[0]];
const result = mergeStoryTaskResult({ rows: current }, target, data, "job");
expect(result.rows[0]).toEqual(current[0]);
expect(result.rows[1]).toEqual({ ...rows[0], imagePrompt: "image 1", motionPrompt: "move 1" });
expect(result.storyTaskResults.job.status).toBe("conflict");
});
test("incomplete prompt batches cannot be marked successful", () => {
const target = { type: "story-prompts" as const, nodeId: "script", rows: [{ id: "r1" }] };
for (const data of [{}, { analysisRows: [] }, { analysisRows: [{ imagePrompt: "x" }] }])
expect(() => requireStoryPromptResult(data, target)).toThrow();
});
test("recovered task results populate a script while retaining the independent output node", () => {
const script = { ...node, id: "script", draftValues: { storyAssets: [asset] } };
const attached = { ...task, request: { ...task.request, attachment } };
const result = reconcileCanvasTasks<TaskNode>([script], [attached], "2026-09-17T01:00:00Z");
expect(result.find((n) => n.id === "script")?.draftValues?.storyAssets[0].imageUrl).toBe(
"https://example.com/1.png",
);
expect(result.some((n) => n.id === "n1")).toBe(true);
expect(reconcileCanvasTasks<TaskNode>(result, [attached], "2026-09-17T01:00:00Z")).toBe(result);
});
// Recovery only queries an existing, server-recorded task; it never submits another job.
import {
canResumeCanvasTask,
getEnhanceCheckpoint,
getVideoCheckpoint,
} from "@/lib/video-canvas/task-recovery";
import { CanvasTaskLeaseLostError } from "@/lib/video-canvas/image-operation";
import { normalizeVideoAnalysisRows } from "@/lib/video-canvas/video-story";
import { pollTaskResult, EnhanceTerminalError } from "@/lib/canvas/enhance-task-poll";
const recoveryTime = Date.parse("2026-09-17T04:00:00Z");
const checkpoint = {
operation: "enhance" as const,
taskId: "saved-upstream",
sourceImageUrl: "https://example.com/source.png",
};
const recoverable = {
status: "interrupted",
createdAt: "2026-09-17T03:00:00Z",
updatedAt: "2026-09-17T03:59:00Z",
attempts: 1,
request: {
kind: "image-operation",
params: { operation: "enhance", imageUrl: checkpoint.sourceImageUrl },
},
result: { upstreamTask: checkpoint },
};
test("recovery enforces checkpoint identity, age, cooldown and bounded attempts", () => {
expect(canResumeCanvasTask(recoverable, recoveryTime)).toBe(true);
for (const changed of [
{ status: "failed" },
{ result: null },
{ attempts: 20 },
{ createdAt: "2026-09-15T00:00:00Z" },
{ updatedAt: "2026-09-17T03:59:50Z" },
{ attempts: 8 },
{
request: {
kind: "image-operation",
params: { operation: "enhance", imageUrl: "https://example.com/other.png" },
},
},
])
expect(canResumeCanvasTask({ ...recoverable, ...changed }, recoveryTime)).toBe(false);
expect(
getEnhanceCheckpoint({ ...recoverable, request: { kind: "video", params: {} } }),
).toBeNull();
});
test("restarting enhancement from its checkpoint makes zero POST calls", async () => {
let posts = 0,
gets = 0;
const post = async () => {
posts++;
throw Error("must not POST");
};
const result = await executeImageOperation(
recoverable.request.params,
"owner",
{
enhance: post,
extend: post,
removeBackground: post,
angleEdit: post,
pollEnhance: async (req) => {
gets++;
expect(new URL(req.url).searchParams.get("taskId")).toBe(checkpoint.taskId);
return Response.json({ success: true, original_url: "https://example.com/recovered.png" });
},
},
{ resume: checkpoint, wait: async () => {} },
);
expect(result.images[0].originalUrl).toContain("recovered.png");
expect(posts).toBe(0);
expect(gets).toBe(1);
});
test("invalid checkpoints and a replaced worker lease cannot reach the provider", async () => {
let calls = 0;
const handler = async () => {
calls++;
throw Error("provider touched");
};
const handlers = {
enhance: handler,
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance: handler,
};
await expect(
executeImageOperation(recoverable.request.params, "owner", handlers, {
resume: { ...checkpoint, sourceImageUrl: "wrong" },
}),
).rejects.toThrow("不匹配");
await expect(
executeImageOperation(recoverable.request.params, "owner", handlers, {
assertLease: async () => {
throw new CanvasTaskLeaseLostError();
},
}),
).rejects.toBeInstanceOf(CanvasTaskLeaseLostError);
expect(calls).toBe(0);
});
test("terminal provider failure stops query recovery instead of staying pending", async () => {
const handler = async () =>
Response.json({ terminal: true, detail: "AOS任务失败: invalid image" }, { status: 500 });
try {
await executeImageOperation(
recoverable.request.params,
"owner",
{
enhance: handler,
extend: handler,
removeBackground: handler,
angleEdit: handler,
pollEnhance: handler,
},
{ resume: checkpoint, wait: async () => {} },
);
throw Error("expected provider failure");
} catch (error) {
expect(error).not.toBeInstanceOf(ImageOperationUncertainError);
expect(String(error)).toContain("AOS任务失败");
}
});
test("actual AOS polling propagates negative state immediately and bounds elapsed retries", async () => {
let calls = 0;
await expect(
pollTaskResult("https://example.com", "qa", "id", 1, 1000, async () => {
calls++;
return Response.json({ data: { state: -1, err_message: "bad image", progress: 0 } });
}),
).rejects.toBeInstanceOf(EnhanceTerminalError);
expect(calls).toBe(1);
const started = Date.now();
await expect(
pollTaskResult("https://example.com", "qa", "id", 1, 25, async () =>
Response.json({ data: { state: 0, progress: 10 } }),
),
).rejects.toThrow("轮询超时");
expect(Date.now() - started).toBeLessThan(500);
});
test("video analysis preserves zero timestamps and all shots without invented camera or audio facts", () => {
const rows = normalizeVideoAnalysisRows(
Array.from({ length: 12 }, (_, i) => ({ sceneDescription: `镜头${i}`, startTime: 0 })),
[],
"story",
);
expect(rows).toHaveLength(12);
expect(rows[0]).toMatchObject({
startTime: "0",
lighting: "",
backgroundMusic: "",
cameraMovement: "",
imagePrompt: "",
});
expect(normalizeVideoAnalysisRows([{}, { startTime: 0, backgroundMusic: "无" }])).toEqual([]);
expect(() =>
requireAnalysisResult({ summary: "没有镜头", analysisRows: [] }, "video-analysis"),
).toThrow("有效镜头");
});
test("video story refresh restores real rows, pending state and errors without a placeholder shot", () => {
const story = {
...node,
kind: "video-story",
inputType: "text",
draftValues: { action: "VIDEO_STORY_ANALYSIS", rows: [] },
};
const analysis = {
...task,
request: {
...task.request,
node: story as any,
kind: "analysis" as const,
params: { mode: "video-analysis" },
},
result: {
summary: "视频内容",
analysisRows: [{ sceneDescription: "杯子", startTime: 0 }],
framePreviews: ["https://example.com/frame.png"],
},
};
const pending = reconcileCanvasTasks(
[story],
[{ ...analysis, status: "running" }],
"2026-09-17T01:00:00Z",
);
expect(pending[0].draftValues).toMatchObject({ status: "loading", rows: [] });
const failed = reconcileCanvasTasks(
pending,
[{ ...analysis, status: "failed", error: "解析失败" }],
"2026-09-17T01:00:00Z",
);
expect(failed[0].draftValues).toMatchObject({ status: "error", error: "解析失败", rows: [] });
const restored = reconcileCanvasTasks<TaskNode>([], [analysis], "2026-09-17T01:00:00Z")[0];
expect(restored.kind).toBe("video-story");
expect(restored.draftValues).toMatchObject({ status: "ready", summary: "视频内容" });
expect(restored.draftValues?.rows[0]).toMatchObject({
startTime: "0",
keyframeUrl: "https://example.com/frame.png",
lighting: "",
});
});
test("video recovery requires an original server checkpoint and matching selected model", () => {
const upstreamTask = {
operation: "video",
provider: "aliyun",
taskId: "video-original",
modelRef: "model-row",
modelId: "wan-test",
providerId: "provider-row",
baseUrl: "https://provider.invalid",
};
const task = {
...recoverable,
request: { kind: "video", params: { preferredModelRef: "model-row" } },
result: { upstreamTask },
};
expect(getVideoCheckpoint(task)).toEqual(upstreamTask);
expect(canResumeCanvasTask(task, recoveryTime)).toBe(true);
for (const value of [
undefined,
{ ...upstreamTask, taskId: "" },
{ ...upstreamTask, provider: "unknown" },
{ ...upstreamTask, modelRef: "other" },
]) {
expect(getVideoCheckpoint({ ...task, result: { upstreamTask: value } })).toBeNull();
expect(canResumeCanvasTask({ ...task, result: { upstreamTask: value } }, recoveryTime)).toBe(
false,
);
}
expect(canResumeCanvasTask({ ...task, attempts: 20 }, recoveryTime)).toBe(false);
});
import { buildAudioRequest, officialAudioBase } from "@/lib/video-canvas/audio-contract";
import { getAudioCheckpoint } from "@/lib/video-canvas/task-recovery";
test("official Alibaba audio requests match each model capabilities", () => {
const tts = buildAudioRequest(
{ mode: "tts", modelRef: "a", text: "你好", voice: "Cherry", instructions: "温柔自然" },
"qwen3-tts-instruct-flash",
);
expect(tts.path).toContain("multimodal-generation");
expect(tts.body.input).toMatchObject({
text: "你好",
voice: "Cherry",
instructions: "温柔自然",
optimize_instructions: true,
});
expect(tts.body.input).not.toHaveProperty("rate");
expect(() =>
buildAudioRequest(
{ mode: "tts", modelRef: "a", text: "你好", voice: "Cherry", rate: 2 },
"qwen3-tts-flash",
),
).toThrow("不支持数值");
expect(() =>
buildAudioRequest(
{ mode: "tts", modelRef: "a", text: "你好", voice: "unsupported" },
"qwen3-tts-flash",
),
).toThrow("音色");
const modern = buildAudioRequest(
{ mode: "tts", modelRef: "a", text: "你好", voice: "longanlingxin", format: "mp3", rate: 1.5 },
"qwen-audio-3.0-tts-plus",
);
expect(modern.body.input).toMatchObject({ format: "mp3", rate: 1.5, language_hints: ["zh"] });
});
test("music validates lyrics and excludes singing fields from instrumental output", () => {
const music = buildAudioRequest(
{
mode: "music",
modelRef: "a",
text: "轻快吉他",
lyrics: "不应该传的歌词",
instrumental: true,
format: "mp3",
},
"fun-music-v1",
);
expect(music.body.input).toEqual({ prompt: "轻快吉他", is_instrumental: true, format: "mp3" });
expect(() =>
buildAudioRequest(
{ mode: "music", modelRef: "a", lyrics: "太短", instrumental: false },
"fun-music-v1",
),
).toThrow("歌词");
expect(() =>
buildAudioRequest({ mode: "music", modelRef: "a", text: "音乐" }, "qwen3-tts-flash"),
).toThrow("不匹配");
});
test("audio credentials can only target official domains and correct region", () => {
expect(
officialAudioBase("https://dashscope.aliyuncs.com/compatible-mode/v1", "fun-music-v1"),
).toBe("https://dashscope.aliyuncs.com");
expect(() => officialAudioBase("https://dashscope.aliyuncs.com.evil.test")).toThrow();
expect(() => officialAudioBase("http://dashscope.aliyuncs.com")).toThrow();
expect(() => officialAudioBase("https://dashscope-intl.aliyuncs.com", "fun-music-v1")).toThrow(
"北京",
);
});
test("only matching persisted audio result may resume archival", () => {
const task = {
request: { kind: "audio", params: { modelRef: "ref" } },
result: {
upstreamAudio: {
operation: "audio",
modelRef: "ref",
modelId: "qwen3-tts-flash",
url: "https://example.com/audio.wav",
format: "wav",
},
},
};
expect(getAudioCheckpoint(task)).toMatchObject({ modelRef: "ref" });
expect(
getAudioCheckpoint({ ...task, request: { kind: "audio", params: { modelRef: "another" } } }),
).toBeNull();
expect(
getAudioCheckpoint({
...task,
result: { upstreamAudio: { ...task.result.upstreamAudio, url: "file:///tmp/private" } },
}),
).toBeNull();
});