import { beforeEach, describe, expect, it, vi } from "vitest"; import { queueTask } from "./queueTask"; describe("queueTask - delayed task handling", () => { const MAX_DELAY_SECONDS = 12 * 60 * 60; // 43,200 seconds let mockEnv: Cloudflare.Env; let kvPutSpy: ReturnType; let queueSendSpy: ReturnType; beforeEach(() => { kvPutSpy = vi.fn(() => Promise.resolve()); queueSendSpy = vi.fn(() => Promise.resolve()); mockEnv = { DELAYED_TASKS: { put: kvPutSpy, get: vi.fn(() => Promise.resolve(null)), delete: vi.fn(() => Promise.resolve()), list: vi.fn(() => Promise.resolve({ keys: [], list_complete: true })), getWithMetadata: vi.fn(() => Promise.resolve({ value: null, metadata: null }), ), } as any, NEW_EPISODE: { send: queueSendSpy, } as any, ANILIST_UPDATES: { send: vi.fn(() => Promise.resolve()), } as any, } as any; // Mock crypto.randomUUID (globalThis as any).crypto = { randomUUID: vi.fn(() => "test-uuid-123") }; }); describe("tasks with delay <= 9 hours", () => { it("queues task directly when delay is less than 9 hours", async () => { await queueTask( "NEW_EPISODE", { aniListId: 123, episodeNumber: 1 }, { scheduleConfig: { delay: { hours: 6 } }, env: mockEnv, }, ); // Should queue directly expect(queueSendSpy).toHaveBeenCalledTimes(1); // Should NOT store in KV expect(kvPutSpy).not.toHaveBeenCalled(); }); it("queues task directly when delay is exactly 9 hours", async () => { await queueTask( "NEW_EPISODE", { aniListId: 456, episodeNumber: 2 }, { scheduleConfig: { delay: { hours: 9 } }, env: mockEnv, }, ); expect(queueSendSpy).toHaveBeenCalledTimes(1); expect(kvPutSpy).not.toHaveBeenCalled(); }); it("passes correct delay to queue", async () => { await queueTask( "NEW_EPISODE", { aniListId: 789, episodeNumber: 3 }, { scheduleConfig: { delay: { hours: 3, minutes: 30 } }, env: mockEnv, }, ); const callArgs = queueSendSpy.mock.calls[0]; expect(callArgs[1].delaySeconds).toBe(3 * 3600 + 30 * 60); }); }); describe("tasks with delay > 9 hours", () => { it("stores task in KV when delay exceeds 9 hours", async () => { await queueTask( "NEW_EPISODE", { aniListId: 111, episodeNumber: 4 }, { scheduleConfig: { delay: { hours: 24 } }, env: mockEnv, }, ); // Should store in KV expect(kvPutSpy).toHaveBeenCalledTimes(1); // Should NOT queue directly expect(queueSendSpy).not.toHaveBeenCalled(); }); it("stores task in KV when delay is 9 hours + 1 second", async () => { await queueTask( "NEW_EPISODE", { aniListId: 222, episodeNumber: 5 }, { scheduleConfig: { delay: { hours: 9, seconds: 1 } }, env: mockEnv, }, ); expect(kvPutSpy).toHaveBeenCalledTimes(1); expect(queueSendSpy).not.toHaveBeenCalled(); }); it("generates correct KV key format", async () => { const nowSeconds = Math.floor(Date.now() / 1000); await queueTask( "NEW_EPISODE", { aniListId: 333, episodeNumber: 6 }, { scheduleConfig: { delay: { hours: 48 } }, env: mockEnv, }, ); const kvKey = kvPutSpy.mock.calls[0][0]; expect(kvKey).toMatch(/^delayed-task:\d+:test-uuid-123$/); // Verify timestamp is approximately correct (within 1 second) const timestampMatch = kvKey.match(/delayed-task:(\d+):/); if (timestampMatch) { const storedTimestamp = parseInt(timestampMatch[1]); const expectedTimestamp = nowSeconds + 48 * 3600; expect(Math.abs(storedTimestamp - expectedTimestamp)).toBeLessThan(2); } }); it("stores correct metadata in KV", async () => { await queueTask( "NEW_EPISODE", { aniListId: 444, episodeNumber: 7 }, { scheduleConfig: { delay: { hours: 36 } }, env: mockEnv, }, ); const kvValue = kvPutSpy.mock.calls[0][1]; const metadata = JSON.parse(kvValue); expect(metadata.queueName).toBe("NEW_EPISODE"); expect(metadata.body).toEqual({ aniListId: 444, episodeNumber: 7 }); expect(metadata.taskId).toBe("test-uuid-123"); expect(metadata.retryCount).toBe(0); expect(metadata.headers).toBeDefined(); expect(metadata.scheduledEpochTime).toBeTypeOf("number"); expect(metadata.createdAt).toBeTypeOf("number"); }); it("throws error when DELAYED_TASKS KV is not available", async () => { const envWithoutKV = { ...mockEnv }; delete (envWithoutKV as any).DELAYED_TASKS; await expect( queueTask( "NEW_EPISODE", { aniListId: 555, episodeNumber: 8 }, { scheduleConfig: { delay: { hours: 24 } }, env: envWithoutKV, }, ), ).rejects.toThrow("DELAYED_TASKS KV namespace not available"); }); }); describe("epoch time scheduling", () => { it("queues directly when epoch time is within 9 hours", async () => { const futureTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now await queueTask( "NEW_EPISODE", { aniListId: 666, episodeNumber: 9 }, { scheduleConfig: { epochTime: futureTime }, env: mockEnv, }, ); expect(queueSendSpy).toHaveBeenCalledTimes(1); expect(kvPutSpy).not.toHaveBeenCalled(); }); it("stores in KV when epoch time is beyond 9 hours", async () => { const futureTime = Math.floor(Date.now() / 1000) + 24 * 3600; // 24 hours from now await queueTask( "NEW_EPISODE", { aniListId: 777, episodeNumber: 10 }, { scheduleConfig: { epochTime: futureTime }, env: mockEnv, }, ); expect(kvPutSpy).toHaveBeenCalledTimes(1); expect(queueSendSpy).not.toHaveBeenCalled(); }); }); });