Files
aniplay-api/src/libs/tasks/queueTask.spec.ts
Rushil Perera 1501aff3b6 fix: adjust task delay threshold to 9 hours
Updates the maximum delay for direct task queuing to 9 hours. This change ensures that tasks with delays exceeding this threshold are stored in KV for later processing.

The update also reflects the new delay threshold in the unit tests.
2025-12-16 08:28:14 -05:00

212 lines
6.1 KiB
TypeScript

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<typeof vi.fn>;
let queueSendSpy: ReturnType<typeof vi.fn>;
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();
});
});
});