244 lines
9.0 KiB
JavaScript
244 lines
9.0 KiB
JavaScript
// Exercises actual provider adapters and selection with synthetic credentials and an in-process fetch stub.
|
|
// No real network request or paid model execution is permitted.
|
|
const fs = require("node:fs"),
|
|
path = require("node:path"),
|
|
assert = require("node:assert/strict");
|
|
(async () => {
|
|
const esbuild = require(
|
|
path.resolve(
|
|
"node_modules/.pnpm",
|
|
fs.readdirSync("node_modules/.pnpm").find((n) => n.startsWith("esbuild@0.25.")),
|
|
"node_modules/esbuild",
|
|
),
|
|
);
|
|
const out = path.resolve(".qa/video-provider-recovery");
|
|
fs.mkdirSync(out, { recursive: true });
|
|
const stubs = {
|
|
"server-only": "",
|
|
"@/lib/db/queries": `export const getEnabledAiModelsWithProvider=async()=>globalThis.qaModels;export const getSubModelsByParentModelId=async()=>[];export const getSystemSetting=async()=>null;export const getDefaultStorageConfig=async()=>{throw Error('unexpected storage');};`,
|
|
"@/lib/ai/dynamic-provider": `export const getLanguageModelById=async()=>{throw Error('durable video must use task adapter');};`,
|
|
"@/lib/storage/service": `export const uploadFile=async()=>{throw Error('unexpected upload');};export const buildS3PublicUrl=()=>'';export const createS3Client=()=>{throw Error('unexpected S3');};export const toDbS3UploadConfig=x=>x;`,
|
|
"@/lib/storage/image-archive": `export const createDeferredUploadedImage=async()=>null;`,
|
|
};
|
|
await esbuild.build({
|
|
entryPoints: ["lib/ai/tools/generate-video.ts"],
|
|
outfile: path.join(out, "video.cjs"),
|
|
bundle: true,
|
|
platform: "node",
|
|
format: "cjs",
|
|
packages: "external",
|
|
alias: { "@": process.cwd() },
|
|
plugins: [
|
|
{
|
|
name: "qa-only",
|
|
setup(build) {
|
|
build.onResolve({ filter: /.*/ }, (a) =>
|
|
stubs[a.path] !== undefined ? { path: a.path, namespace: "qa" } : undefined,
|
|
);
|
|
build.onLoad({ filter: /.*/, namespace: "qa" }, (a) => ({
|
|
contents: stubs[a.path],
|
|
loader: "js",
|
|
}));
|
|
},
|
|
},
|
|
],
|
|
});
|
|
const { executeDirectVideoGeneration } = require(path.join(out, "video.cjs"));
|
|
const originalFetch = globalThis.fetch,
|
|
originalTimeout = globalThis.setTimeout;
|
|
// Only provider sleep timers are shortened. AbortSignal's native timeout is unchanged.
|
|
globalThis.setTimeout = (fn, ms, ...args) => originalTimeout(fn, Math.min(ms, 1), ...args);
|
|
try {
|
|
for (const [kind, type, modelId] of [
|
|
["aliyun", "qianwen-video", "wan2.6-i2v-flash"],
|
|
["vip98k", "vip98k", "veo-qa"],
|
|
["volcano", "volcano-video", "doubao-seedance-1-0-pro-250528"],
|
|
["atlascloud", "atlascloud", "qa-video"],
|
|
["agnes", "custom", "agnes-video-qa"],
|
|
]) {
|
|
const capabilities = {
|
|
kind: "video",
|
|
mediaType: "video",
|
|
provider: kind === "agnes" ? "agnes-ai" : kind,
|
|
supportedModes: ["text-to-video", "first-frame"],
|
|
supportedResolutions: ["720p"],
|
|
supportedRatios: ["16:9"],
|
|
durationRange: { min: 2, max: 15 },
|
|
durationOptions: [5],
|
|
maxReferenceImages: 4,
|
|
};
|
|
const fixture = {
|
|
model: {
|
|
id: "model-" + kind,
|
|
modelId,
|
|
displayName: kind,
|
|
isEnabled: true,
|
|
capabilities,
|
|
imageGenConfig:
|
|
kind === "atlascloud"
|
|
? {
|
|
atlasCloud: {
|
|
submitEndpoint: "/api/v1/model/generateVideo",
|
|
pollEndpoint: "/api/v1/model/prediction/{id}",
|
|
propertyNames: ["model", "prompt", "duration", "aspect_ratio", "resolution"],
|
|
requiredFields: ["model", "prompt"],
|
|
maxReferenceImages: 0,
|
|
defaultValues: {},
|
|
},
|
|
}
|
|
: {},
|
|
},
|
|
provider: {
|
|
id: "provider-" + kind,
|
|
type,
|
|
apiKey: "synthetic-qa-key",
|
|
baseUrl: "https://provider.invalid",
|
|
},
|
|
};
|
|
globalThis.qaModels = [fixture];
|
|
let checkpoint = null,
|
|
calls = [],
|
|
failQuery = true;
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(
|
|
new URL(String(url)).hostname,
|
|
"provider.invalid",
|
|
"unexpected network destination",
|
|
);
|
|
const method = init.method || "GET";
|
|
calls.push(method);
|
|
if (method === "POST") {
|
|
assert.equal(calls.filter((m) => m === "POST").length, 1, "duplicate submit");
|
|
if (kind === "aliyun") return Response.json({ output: { task_id: "qa-original" } });
|
|
if (kind === "vip98k") return Response.json({ task_id: "qa-original" });
|
|
if (kind === "agnes")
|
|
return Response.json({ video_id: "qa-original", task_id: "qa-original" });
|
|
return Response.json({ id: "qa-original" });
|
|
}
|
|
assert(checkpoint, "provider GET happened before durable checkpoint");
|
|
assert(String(url).includes("qa-original"), "query must use saved task");
|
|
if (failQuery) throw Error("QA simulated connection lost");
|
|
if (kind === "aliyun")
|
|
return Response.json({
|
|
output: { task_status: "SUCCEEDED", video_url: "https://output.invalid/video.mp4" },
|
|
});
|
|
if (kind === "volcano")
|
|
return Response.json({
|
|
status: "succeeded",
|
|
content: { video_url: "https://output.invalid/video.mp4" },
|
|
});
|
|
if (kind === "agnes")
|
|
return Response.json({ status: "completed", url: "https://output.invalid/video.mp4" });
|
|
return Response.json({
|
|
status: "completed",
|
|
outputs: ["https://output.invalid/video.mp4"],
|
|
});
|
|
};
|
|
const params = {
|
|
prompt: "QA only",
|
|
mode: kind === "aliyun" ? "first-frame" : "text-to-video",
|
|
images: kind === "aliyun" ? ["https://input.invalid/ref.png"] : [],
|
|
duration: 5,
|
|
resolution: "720p",
|
|
ratio: "16:9",
|
|
preferredModelRef: fixture.model.id,
|
|
};
|
|
await assert.rejects(
|
|
executeDirectVideoGeneration({
|
|
...params,
|
|
taskLifecycle: {
|
|
save: async (cp) => {
|
|
checkpoint = cp;
|
|
},
|
|
assertActive: async () => {},
|
|
},
|
|
}),
|
|
);
|
|
assert(checkpoint, kind + " did not save accepted task");
|
|
assert.equal(checkpoint.provider, kind);
|
|
assert.equal(checkpoint.modelRef, fixture.model.id);
|
|
failQuery = false;
|
|
calls = [];
|
|
const result = await executeDirectVideoGeneration({
|
|
...params,
|
|
taskLifecycle: {
|
|
resume: checkpoint,
|
|
save: async () => {
|
|
throw Error("resume must not save new task");
|
|
},
|
|
assertActive: async () => {},
|
|
},
|
|
});
|
|
assert.equal(result.videoUrl, "https://output.invalid/video.mp4");
|
|
assert(calls.length > 0);
|
|
assert(
|
|
calls.every((method) => method === "GET"),
|
|
kind + " resumed with a POST",
|
|
);
|
|
calls = [];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
assert.equal(new URL(String(url)).hostname, "provider.invalid");
|
|
calls.push(init.method || "GET");
|
|
return Response.json(
|
|
kind === "aliyun"
|
|
? { output: { task_status: "FAILED", code: "QA_FAILURE" } }
|
|
: { status: "failed", error: { message: "QA terminal network failure" } },
|
|
);
|
|
};
|
|
await assert.rejects(
|
|
executeDirectVideoGeneration({
|
|
...params,
|
|
taskLifecycle: { resume: checkpoint, save: async () => {}, assertActive: async () => {} },
|
|
}),
|
|
(error) => error.constructor.name === "ProviderTaskFailedError",
|
|
);
|
|
assert.deepEqual(calls, ["GET"], kind + " retried a terminal failure");
|
|
fixture.provider.baseUrl = "https://changed.invalid";
|
|
calls = [];
|
|
await assert.rejects(
|
|
executeDirectVideoGeneration({
|
|
...params,
|
|
taskLifecycle: { resume: checkpoint, save: async () => {}, assertActive: async () => {} },
|
|
}),
|
|
/线路已变更/,
|
|
);
|
|
assert.equal(calls.length, 0);
|
|
fixture.provider.baseUrl = "https://provider.invalid";
|
|
calls = [];
|
|
globalThis.fetch = async (url, init = {}) => {
|
|
calls.push(init.method || "GET");
|
|
throw Error("fetch failed: connection reset during submit");
|
|
};
|
|
await assert.rejects(
|
|
executeDirectVideoGeneration({
|
|
...params,
|
|
taskLifecycle: {
|
|
save: async () => {
|
|
throw Error("no accepted ID");
|
|
},
|
|
assertActive: async () => {},
|
|
},
|
|
}),
|
|
/不会自动重新提交/,
|
|
);
|
|
assert.equal(
|
|
calls.filter((m) => m === "POST").length,
|
|
1,
|
|
kind + " retried an uncertain POST",
|
|
);
|
|
console.log(
|
|
`PASS ${kind}: checkpoint before GET, interrupted query, GET-only resume, original provider identity enforced`,
|
|
);
|
|
}
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
globalThis.setTimeout = originalTimeout;
|
|
}
|
|
})().then(
|
|
() => process.exit(0),
|
|
(error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
},
|
|
);
|