ai-chatbot/scripts/qa/video-canvas-task-db.cjs

179 lines
6.8 KiB
JavaScript

// Local disposable PostgreSQL only. Install embedded-postgres into .qa/video-task-db first.
const fs = require("fs"),
path = require("path"),
assert = require("node:assert/strict");
(async () => {
const { default: EmbeddedPostgres } = await import(
path.resolve(".qa/video-task-db/node_modules/embedded-postgres/dist/index.js")
);
const pg = new EmbeddedPostgres({
databaseDir: path.resolve(".qa/video-task-db/data"),
port: 55439,
user: "canvas_qa",
password: "local-disposable-only",
persistent: false,
onLog: (message) => console.log(message),
onError: (message) => console.error(message),
});
let client;
try {
await pg.initialise();
await pg.start();
client = pg.getPgClient();
await client.connect();
process.env.POSTGRES_URL =
"postgres://canvas_qa:local-disposable-only@127.0.0.1:55439/postgres";
await client.query(
'CREATE TABLE "User" (id uuid PRIMARY KEY); CREATE TABLE "CanvasProject" (id uuid PRIMARY KEY, "userId" uuid);',
);
await client.query(
fs.readFileSync("lib/db/migrations/0091_add_canvas_capability_tasks.sql", "utf8"),
);
const esbuild = require(
path.resolve(
"node_modules/.pnpm",
fs.readdirSync("node_modules/.pnpm").find((name) => name.startsWith("esbuild@0.25.")),
"node_modules/esbuild",
),
);
await esbuild.build({
entryPoints: ["lib/video-canvas/task-store.ts"],
outfile: ".qa/video-task-db/store.cjs",
bundle: true,
platform: "node",
format: "cjs",
packages: "external",
alias: { "@": process.cwd() },
plugins: [
{
name: "server-only",
setup(build) {
build.onResolve({ filter: /^server-only$/ }, () => ({
path: "empty",
namespace: "qa",
}));
build.onLoad({ filter: /.*/, namespace: "qa" }, () => ({ contents: "" }));
},
},
],
});
const store = require(path.resolve(".qa/video-task-db/store.cjs"));
const user = crypto.randomUUID(),
other = crypto.randomUUID(),
project = crypto.randomUUID();
await client.query('INSERT INTO "User" VALUES ($1),($2)', [user, other]);
await client.query('INSERT INTO "CanvasProject" VALUES ($1,$2)', [project, user]);
assert.equal(await store.ownsTaskProject(user, project), true);
assert.equal(await store.ownsTaskProject(other, project), false);
const request = {
requestId: crypto.randomUUID(),
projectId: project,
kind: "video",
params: { mode: "text-to-video", prompt: "QA" },
node: {
id: "node-a",
title: "QA",
kind: "media",
inputType: "video",
x: 0,
y: 0,
width: 300,
height: 200,
},
edges: [],
};
const copies = await Promise.all(
Array.from({ length: 10 }, () => store.createCanvasGenerationTask(user, request)),
);
assert.equal(new Set(copies.map((task) => task.id)).size, 1);
const task = copies[0];
await assert.rejects(
store.createCanvasGenerationTask(user, {
...request,
params: { ...request.params, prompt: "different" },
}),
);
const claims = await Promise.all(
Array.from({ length: 10 }, () => store.claimCanvasGenerationTask(user, project)),
);
assert.equal(claims.filter(Boolean).length, 1);
assert.equal((await store.cancelCanvasGenerationTask(user, project, task.id)).ok, false);
assert.equal((await store.cancelCanvasGenerationTask(other, project, task.id)).task, null);
const pending = await store.createCanvasGenerationTask(user, {
...request,
requestId: crypto.randomUUID(),
node: { ...request.node, id: "node-b" },
});
assert.equal(await store.claimCanvasGenerationTask(user, project), null);
assert.equal((await store.cancelCanvasGenerationTask(user, project, pending.id)).ok, true);
await store.finishCanvasGenerationTask(task.id, { url: "https://example.com/qa.mp4" });
assert.equal(
(await store.listCanvasGenerationTasks(user, project)).find((t) => t.id === task.id).result
.url,
"https://example.com/qa.mp4",
);
assert.equal((await store.listCanvasGenerationTasks(other, project)).length, 0);
assert.equal(await store.hideCanvasGenerationNode(user, project, task.id, "node-a"), true);
assert.deepEqual(
(await store.listCanvasGenerationTasks(user, project)).find((t) => t.id === task.id).request
.hiddenNodeIds,
["node-a"],
);
const stale = await store.createCanvasGenerationTask(user, {
...request,
requestId: crypto.randomUUID(),
node: { ...request.node, id: "node-c" },
});
await store.claimCanvasGenerationTask(user, project);
await client.query(
"UPDATE \"CanvasCapabilityTask\" SET \"updatedAt\"=(now() at time zone 'UTC')-interval '3 minutes' WHERE id=$1",
[stale.id],
);
assert.equal(
(await store.listCanvasGenerationTasks(user, project)).find((t) => t.id === stale.id).status,
"interrupted",
);
assert.equal(await store.claimCanvasGenerationTask(user, project), null);
await store.checkpointCanvasGenerationTask(stale.id, {
upstreamTask: { taskId: "ignored-after-interruption" },
});
assert.equal(
(await store.listCanvasGenerationTasks(user, project)).find((t) => t.id === stale.id).result,
null,
);
await store.finishCanvasGenerationTask(stale.id, { url: "https://example.com/late.mp4" });
assert.equal(
(await store.listCanvasGenerationTasks(user, project)).find((t) => t.id === stale.id).status,
"succeeded",
);
const upstream = await store.createCanvasGenerationTask(user, {
...request,
requestId: crypto.randomUUID(),
node: { ...request.node, id: "node-upstream" },
});
await store.claimCanvasGenerationTask(user, project);
await store.checkpointCanvasGenerationTask(upstream.id, {
upstreamTask: { operation: "enhance", taskId: "provider-123" },
});
await store.interruptCanvasGenerationTask(upstream.id, "查询连接中断");
const recovered = (await store.listCanvasGenerationTasks(user, project)).find(
(t) => t.id === upstream.id,
);
assert.equal(recovered.status, "interrupted");
assert.equal(recovered.result.upstreamTask.taskId, "provider-123");
assert.equal(await store.claimCanvasGenerationTask(user, project), null);
console.log(
"PASS: real PostgreSQL ownership, idempotency (10 submits), exclusive claiming (10 workers), serial queue, cancellation CAS, durable results, hidden nodes, interrupted task without resubmission, late-result recovery.",
);
} finally {
if (client) await client.end();
await pg.stop();
}
})().then(
() => process.exit(0),
(error) => {
console.error(error);
process.exit(1);
},
);