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.
212 lines
6.1 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|