// Run against a temporary localhost Next process with an intentionally unavailable // SMART_EDIT_RENDER_URL. Creates and deletes its own canvas; no model work starts. const assert = require("node:assert/strict"); const { randomUUID } = require("node:crypto"); const base = process.env.CANVAS_QA_URL || "http://127.0.0.1:3141"; assert(["localhost", "127.0.0.1"].includes(new URL(base).hostname)); const cookies = new Map(); async function request(route, body, method = body ? "POST" : "GET") { const response = await fetch(base + route, { method, redirect: "manual", headers: { "content-type": "application/json", cookie: [...cookies].map(([key, value]) => `${key}=${value}`).join("; ") }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(20000) }); for (const cookie of response.headers.getSetCookie()) { const part = cookie.split(";")[0], equal = part.indexOf("="); cookies.set(part.slice(0, equal), part.slice(equal + 1)); } return { status: response.status, body: await response.json().catch(() => null) }; } (async () => { let projectId; try { const auth = await request("/api/auth/guest?redirectUrl=" + encodeURIComponent(base + "/video-canvas")); assert([302, 303, 307].includes(auth.status), `guest login ${auth.status}`); const project = await request("/api/canvas", { title: "QA 服务故障零生成验收", workspaceType: "video" }); assert.equal(project.status, 200); projectId = project.body.data.id; const node = { id: "qa-render", title: "QA", kind: "media", inputType: "video", x: 0, y: 0, width: 400, height: 300 }; const asset = { id: "asset", name: "QA", url: "https://example.com/qa.mp4", duration: 2e6, width: 1280, height: 720, hasAudio: false, hash: "a".repeat(64), subtitles: [] }; const plan = { version: 1, title: "QA", assets: [asset], clips: [{ id: "clip", assetId: "asset", sourceIn: 0, sourceOut: 1e6 }], ratio: "16:9", resolution: "720p", transition: 0, subtitles: false }; const render = { requestId: randomUUID(), projectId, kind: "smart-edit", node, params: { operation: "render", plan }, edges: [] }; const result = await request("/api/canvas/generation-tasks", render); assert.equal(result.status, 503, JSON.stringify(result)); assert.equal(result.body.code, "CANVAS_SERVICE_UNAVAILABLE"); const runId = randomUUID(), groupId = "qa-group"; const image = { requestId: randomUUID(), projectId, kind: "image", node: { ...node, id: "qa-image", inputType: "image" }, params: { prompt: "Must never run while renderer is down" }, workflow: { runId, groupId, inputs: [] }, edges: [] }; const groupedRender = { ...render, requestId: randomUUID(), workflow: { runId, groupId, inputs: [] } }; const batch = await request("/api/canvas/group-runs", [image, groupedRender]); assert.equal(batch.status, 503, JSON.stringify(batch)); assert.equal(batch.body.code, "CANVAS_SERVICE_UNAVAILABLE"); const tasks = await request("/api/canvas/generation-tasks?projectId=" + projectId); assert.equal(tasks.status, 200); assert.equal(tasks.body.tasks.length, 0, "failure must not create any upstream paid tasks"); if (process.env.CANVAS_QA_STALE_VIDEO_MODEL) { const video = { requestId: randomUUID(), projectId, kind: "video", node: { ...node, id: "qa-video" }, params: { prompt: "Must never run with an invalid published contract", preferredModelRef: process.env.CANVAS_QA_STALE_VIDEO_MODEL, ratio: "9:16", resolution: "720p", duration: 4 }, workflow: { runId: randomUUID(), groupId: "qa-stale-video", inputs: [] }, edges: [] }; const stale = await request("/api/canvas/group-runs", [{ ...image, requestId: randomUUID(), workflow: video.workflow }, video]); assert.equal(stale.status, 503, JSON.stringify(stale)); assert.equal(stale.body.code, "CANVAS_SERVICE_UNAVAILABLE"); assert.match(stale.body.message || stale.body.error || "", /视频模型线路检查未通过/); const after = await request("/api/canvas/generation-tasks?projectId=" + projectId); assert.equal(after.body.tasks.length, 0, "stale video specs must not start paid keyframes"); console.log("PASS: live supplier contract rejects stale video specs before paid keyframes start"); } console.log("PASS: real authentication + database + HTTP; single and group outage rejection; zero generation tasks"); } finally { if (projectId) assert.equal((await request("/api/canvas/" + projectId, undefined, "DELETE")).status, 200); } })().catch(error => { console.error(error.message); process.exitCode = 1; });