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