import { expect, test } from "bun:test"; import { forEachDialect } from "@sttracker/db/testing"; import { enqueue, startScheduler, startWorker } from "../src/jobs.ts"; import { makeConnection, makeUser } from "./sync/helpers.ts"; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); forEachDialect("jobs", (getDb) => { test("enqueue with same lockKey returns null", async () => { const db = getDb(); expect(await enqueue(db, "sync", {}, { lockKey: "sync:a" })).toBeString(); expect(await enqueue(db, "sync", {}, { lockKey: "sync:a" })).toBeNull(); expect(await enqueue(db, "sync", {}, { lockKey: "sync:b" })).toBeString(); expect(await enqueue(db, "x", {})).toBeString(); expect(await enqueue(db, "x", {})).toBeString(); }); test("concurrent enqueue with one lockKey admits exactly one", async () => { const db = getDb(); const ids = await Promise.all(Array.from({ length: 6 }, () => enqueue(db, "sync", {}, { lockKey: "k" }))); expect(ids.filter((i) => i !== null)).toHaveLength(1); }); test("worker runs a job and releases the lock", async () => { const db = getDb(); const seen: unknown[] = []; const id = await enqueue(db, "t", { n: 1 }, { lockKey: "l" }); const w = startWorker(db, { t: async (p) => void seen.push(p) }, { pollMs: 10 }); await sleep(150); await w.stop(); expect(seen).toEqual([{ n: 1 }]); expect((await db.k.selectFrom("jobs").selectAll().where("id", "=", id!).executeTakeFirstOrThrow()).status).toBe("done"); expect(await enqueue(db, "t", {}, { lockKey: "l" })).toBeString(); }); test("a job is claimed by only one of two workers", async () => { const db = getDb(); let runs = 0; const h = { t: async () => { runs++; await sleep(30); } }; await enqueue(db, "t", {}); const a = startWorker(db, h, { pollMs: 5 }); const b = startWorker(db, h, { pollMs: 5 }); await sleep(200); await Promise.all([a.stop(), b.stop()]); expect(runs).toBe(1); }); test("worker retries then fails", async () => { const db = getDb(); let calls = 0; const id = (await enqueue(db, "bad", {}))!; const w = startWorker(db, { bad: async () => { calls++; throw new Error("nope"); } }, { pollMs: 10 }); const row = () => db.k.selectFrom("jobs").selectAll().where("id", "=", id).executeTakeFirstOrThrow(); for (let attempt = 1; attempt <= 5; attempt++) { for (let i = 0; i < 100 && (await row()).attempts < attempt; i++) await sleep(10); const r = await row(); expect(r.attempts).toBe(attempt); expect(r.last_error).toBe("nope"); if (attempt < 5) { expect(r.status).toBe("queued"); const delay = Date.parse(r.run_at) - Date.now(); expect(delay).toBeGreaterThan(2 ** attempt * 60_000 - 5_000); expect(delay).toBeLessThanOrEqual(2 ** attempt * 60_000); await db.k.updateTable("jobs").set({ run_at: new Date(Date.now() - 1000).toISOString() }).where("id", "=", id).execute(); } } await w.stop(); expect((await row()).status).toBe("failed"); expect(calls).toBe(5); }); test("scheduler enqueues due connections only", async () => { const db = getDb(); const u = await makeUser(db); const due = await makeConnection(db, u, "p"); const fresh = await makeConnection(db, u, "p"); const manual = await makeConnection(db, u, "p"); const paused = await makeConnection(db, u, "p"); const old = new Date(Date.now() - 120 * 60_000).toISOString(); const recent = new Date(Date.now() - 5 * 60_000).toISOString(); const set = (id: string, v: object) => db.k.updateTable("source_connections").set(v).where("id", "=", id).execute(); await set(due, { schedule_minutes: 60, last_run_at: old }); await set(fresh, { schedule_minutes: 60, last_run_at: recent }); await set(paused, { schedule_minutes: 60, last_run_at: old, status: "paused" }); void manual; const s = startScheduler(db, { tickMs: 3_600_000 }); await s.tickNow(); await s.tickNow(); s.stop(); const jobs = await db.k.selectFrom("jobs").selectAll().execute(); expect(jobs.map((j) => j.lock_key)).toEqual([`sync:${due}`]); expect(JSON.parse(jobs[0]!.payload)).toEqual({ connectionId: due }); }); });