ai-chatbot/scripts/qa/video-canvas-provider-recovery.cjs

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