import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { InMemoryQueue, setQueue, resetQueue } from "./queue"; import { processJob, startWorker, stopWorker } from "./worker"; import { setHandlers } from "./handlers"; import type { Job } from "./queue"; describe("worker", () => { let queue: InMemoryQueue; beforeEach(() => { queue = new InMemoryQueue(); setQueue(queue); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); resetQueue(); }); describe("processJob", () => { it("dispatches darkwatch.scan to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "darkwatch.scan": handler }); const job = await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }); await processJob(job); expect(handler).toHaveBeenCalledWith({ userId: "u1", subscriptionId: "s1" }); }); it("dispatches hometitle.scan to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "hometitle.scan": handler }); const job = await queue.enqueue("hometitle.scan", { userId: "u1", subscriptionId: "s1" }); await processJob(job); expect(handler).toHaveBeenCalledWith({ userId: "u1", subscriptionId: "s1" }); }); it("dispatches reports.generate to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "reports.generate": handler }); const job = await queue.enqueue("reports.generate", { userId: "u1", reportType: "MONTHLY_PLUS" }); await processJob(job); expect(handler).toHaveBeenCalledWith({ userId: "u1", reportType: "MONTHLY_PLUS" }); }); it("dispatches notifications.send to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "notifications.send": handler }); const job = await queue.enqueue("notifications.send", { userId: "u1", channel: "email" }); await processJob(job); expect(handler).toHaveBeenCalledWith({ userId: "u1", channel: "email" }); }); it("dispatches removebrokers.process to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "removebrokers.process": handler }); const job = await queue.enqueue("removebrokers.process", {}); await processJob(job); expect(handler).toHaveBeenCalledWith({}); }); it("dispatches voiceprint.batch to correct handler", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "voiceprint.batch": handler }); const job = await queue.enqueue("voiceprint.batch", { userId: "u1" }); await processJob(job); expect(handler).toHaveBeenCalledWith({ userId: "u1" }); }); it("marks job as completed on success", async () => { setHandlers({ "darkwatch.scan": vi.fn().mockResolvedValue(undefined) }); const job = await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }); await processJob(job); const updated = await queue.getJob(job.id); expect(updated?.status).toBe("completed"); }); it("schedules retry on failure with exponential backoff", async () => { const handler = vi.fn().mockRejectedValue(new Error("Scan failed")); setHandlers({ "darkwatch.scan": handler }); const job = await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }); await processJob(job); // After first retry, job should be pending with attempt 1 const updated = await queue.getJob(job.id); expect(updated?.attempts).toBe(1); expect(updated?.status).toBe("pending"); // Retry delay should be 60s for first retry expect(handler).toHaveBeenCalledTimes(1); }); it("marks as failed after exhausting retries", async () => { const handler = vi.fn().mockRejectedValue(new Error("Final failure")); setHandlers({ "darkwatch.scan": handler }); const job = await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }, { maxAttempts: 2 }); // First attempt - retries (schedules retry) await processJob(job); expect(handler).toHaveBeenCalledTimes(1); expect(job.attempts).toBe(1); // Advance past the retry delay so the retry is available vi.advanceTimersByTime(60001); // Second attempt - dequeue and process const retried = await queue.dequeue(); expect(retried).not.toBeNull(); if (retried) { await processJob(retried); } // After max attempts, job should be failed const failed = await queue.getJob(job.id); expect(failed?.status).toBe("failed"); expect(failed?.error).toBe("Final failure"); }); }); describe("startWorker / stopWorker", () => { it("polls queue and processes jobs", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "darkwatch.scan": handler }); await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }); startWorker({ pollInterval: 50 }); await vi.advanceTimersByTimeAsync(100); expect(handler).toHaveBeenCalled(); await stopWorker(); }); it("stops polling after stopWorker", async () => { const handler = vi.fn().mockResolvedValue(undefined); setHandlers({ "darkwatch.scan": handler }); startWorker({ pollInterval: 50 }); await stopWorker(); await queue.enqueue("darkwatch.scan", { userId: "u1", subscriptionId: "s1" }); await vi.advanceTimersByTimeAsync(200); expect(handler).not.toHaveBeenCalled(); }); }); });