66 lines
3.5 KiB
TypeScript
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");
|
|
});
|