ai-chatbot/tests/routes/canvas-capability-task-recovery.test.ts

66 lines
3.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { serverLoader } from "../helpers/canvas-download-server-loader";
function harness(status = "running") {
let now = 2_000_000;
const row: any = { id: "task-a", requestId: "request-a", userId: "user-a", projectId: "project-a", status, attempts: status === "pending" ? 0 : 1, updatedAt: new Date(now - 16 * 60_000), error: null };
const matches = (predicate: any): boolean => !predicate || (predicate[0] === "and" ? predicate.slice(1).every(matches)
: predicate[0] === "eq" ? row[predicate[1]] === predicate[2]
: predicate[0] === "lt" ? row[predicate[1]] < predicate[2]
: predicate[2].includes(row[predicate[1]]));
const db = {
select: () => ({ from: () => ({ where: (p: any) => ({ limit: async () => matches(p) ? [{ ...row }] : [] }) }) }),
insert: () => ({ values: () => ({ onConflictDoNothing: async () => undefined }) }),
update: () => ({ set: (values: any) => ({ where: (p: any) => {
const apply = () => {
if (!matches(p)) return [];
for (const [key, value] of Object.entries(values)) row[key] = key === "attempts" ? row.attempts + 1 : value;
return [{ ...row }];
};
const result = apply();
return Object.assign(Promise.resolve(result), { returning: async () => result });
} }) }),
};
const columns = Object.fromEntries(["id", "requestId", "userId", "projectId", "status", "updatedAt", "attempts"].map(k => [k, k]));
const store = serverLoader({
"@/lib/db": { db }, "@/lib/db/schema": { canvasCapabilityTask: columns },
"drizzle-orm": { eq: (...v: any[]) => ["eq", ...v], lt: (...v: any[]) => ["lt", ...v], inArray: (...v: any[]) => ["in", ...v], and: (...v: any[]) => ["and", ...v], sql: () => "increment" },
Date: class extends Date { constructor(value?: any) { super(value ?? now); } static now() { return now; } },
})("lib/canvas/capabilities/task-store.ts");
const lookup = { requestId: "request-a", userId: "user-a", projectId: "project-a" };
return { row, store, lookup, advance: (ms: number) => { now += ms; } };
}
test("a stale task stops reporting running without authorizing another execution", async () => {
const h = harness();
const task = await h.store.getCanvasCapabilityTask(h.lookup);
expect(task.status).toBe("failed");
expect(task.error).toContain("结果尚未确认");
const repeated = await h.store.createOrRecoverCanvasCapabilityTask({ ...h.lookup, command: { requestId: "request-a" } });
expect(repeated.status).toBe("failed");
expect(repeated.attempts).toBe(1);
});
test("heartbeats keep a legitimate long-running task active beyond fifteen minutes", async () => {
const h = harness();
await h.store.heartbeatCanvasCapabilityTask("task-a");
h.advance(14 * 60_000);
await h.store.heartbeatCanvasCapabilityTask("task-a");
h.advance(14 * 60_000);
expect((await h.store.getCanvasCapabilityTask(h.lookup)).status).toBe("running");
expect(h.row.attempts).toBe(1);
});
test("only the pending task can be claimed and a duplicate claim cannot run again", async () => {
const h = harness("pending");
expect(await h.store.markCanvasCapabilityTaskRunning("task-a")).toMatchObject({ status: "running", attempts: 1 });
expect(await h.store.markCanvasCapabilityTaskRunning("task-a")).toBeNull();
expect(h.row.attempts).toBe(1);
});
test("another user cannot inspect or change a stale task", async () => {
const h = harness();
expect(await h.store.getCanvasCapabilityTask({ ...h.lookup, userId: "user-b" })).toBeNull();
expect(h.row.status).toBe("running");
});