Files
Kordant/web/src/server/jobs/queue.test.ts
Michael Freno eb8e57c674 feat: implement background job system with queue, worker, scheduler, and handlers
- Add job queue abstraction (InMemoryQueue and Redis/BullMQ adapter)
- Add polling worker with retry logic and exponential backoff
- Add 6 job handlers: darkwatch.scan, voiceprint.batch, hometitle.scan,
  removebrokers.process, reports.generate, notifications.send
- Add cron-based scheduler with tier-appropriate frequencies
  (Basic/Plus/Premium)
- Add tRPC scheduler router for admin (runJobNow, getJobStatus, etc.)
- Add entry point with graceful shutdown support
- Achieve 100% test pass rate for new job system
2026-05-25 17:16:21 -04:00

103 lines
3.3 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { InMemoryQueue } from "./queue";
describe("InMemoryQueue", () => {
let queue: InMemoryQueue;
beforeEach(() => {
queue = new InMemoryQueue();
});
it("enqueues a job with correct payload", async () => {
const job = await queue.enqueue("darkwatch.scan", {
userId: "user-1",
subscriptionId: "sub-1",
});
expect(job.type).toBe("darkwatch.scan");
expect(job.payload).toEqual({ userId: "user-1", subscriptionId: "sub-1" });
expect(job.status).toBe("pending");
expect(job.attempts).toBe(0);
expect(job.maxAttempts).toBe(3);
expect(job.id).toBeDefined();
});
it("dequeues pending jobs in order", async () => {
await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" });
await queue.enqueue("hometitle.scan", { userId: "u2", subscriptionId: "s2" });
const job1 = await queue.dequeue();
expect(job1?.type).toBe("darkwatch.scan");
expect(job1?.status).toBe("running");
const job2 = await queue.dequeue();
expect(job2?.type).toBe("hometitle.scan");
expect(job2?.status).toBe("running");
const job3 = await queue.dequeue();
expect(job3).toBeNull();
});
it("only dequeues pending jobs", async () => {
const job = await queue.enqueue("notifications.send", { userId: "u1", channel: "email" });
await queue.markComplete(job.id);
const result = await queue.dequeue();
expect(result).toBeNull();
});
it("marks job as completed", async () => {
const job = await queue.enqueue("reports.generate", { userId: "u1", reportType: "MONTHLY_PLUS" });
await queue.markComplete(job.id);
const fetched = await queue.getJob(job.id);
expect(fetched?.status).toBe("completed");
});
it("marks job as failed with error", async () => {
const job = await queue.enqueue("voiceprint.batch", {});
await queue.markFailed(job.id, "Test error");
const fetched = await queue.getJob(job.id);
expect(fetched?.status).toBe("failed");
expect(fetched?.error).toBe("Test error");
});
it("returns null for non-existent job", async () => {
const job = await queue.getJob("non-existent");
expect(job).toBeNull();
});
it("filters jobs by status", async () => {
const j1 = await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" });
const j2 = await queue.enqueue("hometitle.scan", { userId: "u2", subscriptionId: "s2" });
await queue.markComplete(j1.id);
const pending = await queue.getJobs("pending");
expect(pending).toHaveLength(1);
expect(pending[0].id).toBe(j2.id);
const completed = await queue.getJobs("completed");
expect(completed).toHaveLength(1);
expect(completed[0].id).toBe(j1.id);
});
it("supports delayed enqueue", async () => {
await queue.enqueue("notifications.send", { userId: "u1", channel: "email" }, { delay: 50 });
const immediate = await queue.dequeue();
expect(immediate).toBeNull();
await new Promise((r) => setTimeout(r, 60));
const delayed = await queue.dequeue();
expect(delayed?.type).toBe("notifications.send");
});
it("supports custom maxAttempts", async () => {
const job = await queue.enqueue("removebrokers.process", {}, { maxAttempts: 5 });
expect(job.maxAttempts).toBe(5);
});
});