217 lines
6.9 KiB
TypeScript
217 lines
6.9 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtempSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-video-dashscope-"));
|
|
|
|
const { handleVideoGeneration } = await import("../../open-sse/handlers/videoGeneration.ts");
|
|
const { VIDEO_PROVIDERS } = await import("../../open-sse/config/videoRegistry.ts");
|
|
|
|
// Makes poll-interval waits resolve instantly so tests don't sleep.
|
|
function immediateTimeout(callback, _ms, ...args) {
|
|
if (typeof callback === "function") callback(...args);
|
|
return 0;
|
|
}
|
|
|
|
const CREATE_URL =
|
|
"https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis";
|
|
const POLL_URL_PREFIX = "https://dashscope-intl.aliyuncs.com/api/v1/tasks/";
|
|
|
|
function jsonResponse(body, status = 200) {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
}
|
|
|
|
test("VIDEO_PROVIDERS exposes the alibaba dashscope-video entry", () => {
|
|
assert.ok(VIDEO_PROVIDERS.alibaba, "alibaba video provider is registered");
|
|
assert.equal(VIDEO_PROVIDERS.alibaba.format, "dashscope-video");
|
|
assert.ok(
|
|
VIDEO_PROVIDERS.alibaba.models.some((m) => m.id === "wan2.7-t2v"),
|
|
"wan2.7-t2v is listed"
|
|
);
|
|
});
|
|
|
|
test("handleVideoGeneration creates + polls a DashScope task and returns mp4 URL", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
let createRequest;
|
|
|
|
globalThis.setTimeout = immediateTimeout;
|
|
globalThis.fetch = async (url, options = {}) => {
|
|
const stringUrl = String(url);
|
|
|
|
if (stringUrl === CREATE_URL) {
|
|
createRequest = {
|
|
url: stringUrl,
|
|
headers: options.headers,
|
|
body: JSON.parse(String(options.body || "{}")),
|
|
};
|
|
return jsonResponse({
|
|
output: { task_id: "ds-task-1", task_status: "PENDING" },
|
|
request_id: "req-1",
|
|
});
|
|
}
|
|
|
|
if (stringUrl.startsWith(POLL_URL_PREFIX)) {
|
|
return jsonResponse({
|
|
output: {
|
|
task_status: "SUCCEEDED",
|
|
video_url: "https://dashscope-cdn.example.com/wan-out.mp4",
|
|
},
|
|
request_id: "req-2",
|
|
usage: { video_count: 1 },
|
|
});
|
|
}
|
|
|
|
throw new Error(`Unexpected URL: ${stringUrl}`);
|
|
};
|
|
|
|
try {
|
|
const result = await handleVideoGeneration({
|
|
body: {
|
|
model: "alibaba/wan2.7-t2v",
|
|
prompt: "a neon city in the rain",
|
|
negative_prompt: "blurry",
|
|
aspect_ratio: "16:9",
|
|
duration: 5,
|
|
},
|
|
credentials: { apiKey: "dashscope-key" },
|
|
log: null,
|
|
});
|
|
|
|
// Create request shape
|
|
assert.equal(createRequest.headers["X-DashScope-Async"], "enable");
|
|
assert.equal(createRequest.headers["Authorization"], "Bearer dashscope-key");
|
|
assert.equal(createRequest.body.model, "wan2.7-t2v");
|
|
assert.equal(createRequest.body.input.prompt, "a neon city in the rain");
|
|
assert.equal(createRequest.body.input.negative_prompt, "blurry");
|
|
// aspect_ratio "16:9" → DashScope size "1280*720"
|
|
assert.equal(createRequest.body.parameters.size, "1280*720");
|
|
assert.equal(createRequest.body.parameters.duration, 5);
|
|
|
|
// Response shape
|
|
assert.equal(result.success, true);
|
|
assert.equal(result.data.data[0].url, "https://dashscope-cdn.example.com/wan-out.mp4");
|
|
assert.equal(result.data.data[0].format, "mp4");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
}
|
|
});
|
|
|
|
test("handleVideoGeneration rejects DashScope requests without credentials", async () => {
|
|
const result = await handleVideoGeneration({
|
|
body: { model: "alibaba/wan2.7-t2v", prompt: "x" },
|
|
credentials: null,
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 401);
|
|
assert.match(result.error, /DashScope API key is required/);
|
|
});
|
|
|
|
test("handleVideoGeneration surfaces a 502 when DashScope returns no task_id", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => jsonResponse({ message: "Invalid API key", request_id: "x" }, 401);
|
|
|
|
try {
|
|
const result = await handleVideoGeneration({
|
|
body: { model: "alibaba/wan2.7-t2v", prompt: "x" },
|
|
credentials: { apiKey: "bad-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 502);
|
|
assert.equal(result.error, "Invalid API key");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("handleVideoGeneration returns 502 when the DashScope task FAILED", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
globalThis.setTimeout = immediateTimeout;
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const stringUrl = String(url);
|
|
if (stringUrl === CREATE_URL) {
|
|
return jsonResponse({ output: { task_id: "ds-fail", task_status: "PENDING" } });
|
|
}
|
|
if (stringUrl.startsWith(POLL_URL_PREFIX)) {
|
|
return jsonResponse({
|
|
output: { task_status: "FAILED", message: "content policy violation" },
|
|
});
|
|
}
|
|
throw new Error(`Unexpected URL: ${stringUrl}`);
|
|
};
|
|
|
|
try {
|
|
const result = await handleVideoGeneration({
|
|
body: { model: "alibaba/wan2.7-t2v", prompt: "x" },
|
|
credentials: { apiKey: "dashscope-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 502);
|
|
assert.equal(result.error, "content policy violation");
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
}
|
|
});
|
|
|
|
test("handleVideoGeneration returns 504 when the DashScope task never completes", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const originalSetTimeout = globalThis.setTimeout;
|
|
const originalNow = Date.now;
|
|
globalThis.setTimeout = immediateTimeout;
|
|
|
|
// Deterministic clock: start at 1000, allow exactly one poll iteration, then
|
|
// jump past the deadline so the while-loop exits on the next check.
|
|
let nowCalls = 0;
|
|
Date.now = () => {
|
|
nowCalls += 1;
|
|
return nowCalls === 1 ? 1000 : nowCalls === 2 ? 2000 : 1_000_000;
|
|
};
|
|
|
|
globalThis.fetch = async (url) => {
|
|
const stringUrl = String(url);
|
|
if (stringUrl === CREATE_URL) {
|
|
return jsonResponse({ output: { task_id: "ds-stuck", task_status: "PENDING" } });
|
|
}
|
|
if (stringUrl.startsWith(POLL_URL_PREFIX)) {
|
|
return jsonResponse({ output: { task_status: "RUNNING" } });
|
|
}
|
|
throw new Error(`Unexpected URL: ${stringUrl}`);
|
|
};
|
|
|
|
try {
|
|
const result = await handleVideoGeneration({
|
|
body: {
|
|
model: "alibaba/wan2.7-t2v",
|
|
prompt: "x",
|
|
timeout_ms: 5000,
|
|
poll_interval_ms: 100,
|
|
},
|
|
credentials: { apiKey: "dashscope-key" },
|
|
log: null,
|
|
});
|
|
|
|
assert.equal(result.success, false);
|
|
assert.equal(result.status, 504);
|
|
assert.match(result.error, /timed out/);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalSetTimeout;
|
|
Date.now = originalNow;
|
|
}
|
|
});
|