Files
UHD_Media_Tool/packages/tasks/test/op-ledger.test.ts
T
rootandClaude Fable 5.1 304feea141 chore(deno): migrate toolchain from Node 24 + pnpm to Deno 2.9.7 + TypeScript 7.0.2 (W1.5)
- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock;
  remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports)
- tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions),
  test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types
- tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl
- server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting;
  drop @hono/node-server, tsx, esbuild build.mjs
- logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests
- relative imports get explicit extensions (sites/mp-sync paths left for follow-up)
- crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas
- single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml
- docs: README, runbook, ADR-001 superseded, plan §1 validation row

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-27 18:53:06 +08:00

106 lines
3.5 KiB
TypeScript

import { sql } from "kysely";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { FencingError, getOperation, migrateTasksTables, withAttemptToken } from "../src/index.ts";
import { connectDb, dropSchemas, pgAvailable, resetSchemas, type TestDb, testSchemas } from "./helpers.ts";
const schemas = testSchemas("ledger");
const hasPg = await pgAvailable();
describe.skipIf(!hasPg)("withAttemptToken(操作账本 fencing)", () => {
let db: TestDb;
beforeAll(async () => {
await resetSchemas(schemas);
db = connectDb(schemas.biz);
await migrateTasksTables(db);
await sql`CREATE TABLE download_task (id text PRIMARY KEY, hash text)`.execute(db);
});
afterAll(async () => {
await db?.destroy();
await dropSchemas(schemas);
});
it("成功后记录结果;再次调用直接返回结果不重做", async () => {
let calls = 0;
const run = () =>
withAttemptToken(
db,
"op-1",
async (info) => {
calls++;
return { hash: "h1", attempt: info.attempt };
},
{
runId: "r1",
commit: async (trx, result) => {
await sql`INSERT INTO download_task (id, hash) VALUES ('d1', ${result.hash})`.execute(trx);
},
},
);
expect(await run()).toEqual({ hash: "h1", attempt: 1 });
expect(await run()).toEqual({ hash: "h1", attempt: 1 });
expect(calls).toBe(1);
const row = await getOperation(db, "op-1");
expect(row).toMatchObject({ status: "succeeded", attempts: 1, runId: "r1" });
});
it("租约丢失:旧 attempt 执行完后提交被拒绝,新 attempt 的结果生效", async () => {
let releaseOld!: () => void;
const oldGate = new Promise<void>((r) => {
releaseOld = r;
});
let oldStarted!: () => void;
const oldStartedP = new Promise<void>((r) => {
oldStarted = r;
});
// 旧 worker(被 SIGSTOP 之类卡住):已开始执行,尚未提交
const oldAttempt = withAttemptToken(
db,
"op-2",
async () => {
oldStarted();
await oldGate;
return "old";
},
{
commit: async (trx) => void (await sql`INSERT INTO download_task (id, hash) VALUES ('d2', 'old')`.execute(trx)),
},
);
await oldStartedP;
// 新 worker 接管同一操作:resumed=true,需要先对账外部状态
let resumed = false;
const fresh = await withAttemptToken(
db,
"op-2",
async (info) => {
resumed = info.resumed;
return "new";
},
{
commit: async (trx) => void (await sql`INSERT INTO download_task (id, hash) VALUES ('d2', 'new')`.execute(trx)),
},
);
expect(fresh).toBe("new");
expect(resumed).toBe(true);
releaseOld();
await expect(oldAttempt).rejects.toBeInstanceOf(FencingError);
const r = await sql<{ hash: string }>`SELECT hash FROM download_task WHERE id = 'd2'`.execute(db);
expect(r.rows).toEqual([{ hash: "new" }]);
expect(await getOperation(db, "op-2")).toMatchObject({ status: "succeeded", attempts: 2, result: "new" });
});
it("fn 抛错:记录 last_error,不写结果,可重试", async () => {
await expect(
withAttemptToken(db, "op-3", async () => {
throw new Error("qb timeout");
}),
).rejects.toThrow("qb timeout");
expect(await getOperation(db, "op-3")).toMatchObject({ status: "started", lastError: "qb timeout" });
expect(await withAttemptToken(db, "op-3", async (i) => i.attempt)).toBe(2);
});
});