Files
calendar/packages/lib/tasker/Tasker.test.ts
T
MorganGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
bca6615da9 test: add unit tests for Tasker class (#26975)
- Test that params are properly passed to async tasker methods
- Test that params are passed in correct order
- Test fallback to sync tasker when async tasker fails
- Test params are passed correctly to sync tasker fallback
- Test error handling when both taskers fail
- Test logging of dispatch information with args
- Test behavior when async tasker is disabled (missing env vars)
- Test error details logging

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-18 11:06:43 +00:00

491 lines
16 KiB
TypeScript

import process from "node:process";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import type { ILogger } from "./types";
vi.mock("@trigger.dev/sdk", () => ({
configure: vi.fn(),
}));
vi.mock("../constants", () => ({
ENABLE_ASYNC_TASKER: true,
}));
vi.mock("../redactError", () => ({
redactError: (err: unknown) => err,
}));
const createMockLogger = (): ILogger => ({
log: vi.fn(),
silly: vi.fn(),
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
getSubLogger: vi.fn(),
});
interface MockTaskerInterface {
taskWithParams: (param1: string, param2: number, param3: { nested: boolean }) => Promise<string>;
simpleTask: () => Promise<void>;
taskThatFails: () => Promise<string>;
}
describe("Tasker", () => {
const originalEnv = process.env;
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
describe("when async tasker is enabled", () => {
beforeEach(() => {
process.env.TRIGGER_SECRET_KEY = "test-secret-key";
process.env.TRIGGER_API_URL = "https://test-trigger-api.com";
});
it("should pass params correctly to the async tasker method", async () => {
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("async failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-fallback-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const param1 = "test-string";
const param2 = 42;
const param3 = { nested: true };
const result = await tasker.dispatch("taskWithParams", param1, param2, param3);
expect(mockAsyncTasker.taskWithParams).toHaveBeenCalledTimes(1);
expect(mockAsyncTasker.taskWithParams).toHaveBeenCalledWith(param1, param2, param3);
expect(result).toBe("async-result");
expect(mockSyncTasker.taskWithParams).not.toHaveBeenCalled();
});
it("should pass multiple params in correct order", async () => {
const { Tasker } = await import("./Tasker");
const receivedParams: unknown[] = [];
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockImplementation((...args) => {
receivedParams.push(...args);
return Promise.resolve("result");
}),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
await tasker.dispatch("taskWithParams", "first", 123, { nested: false });
expect(receivedParams).toEqual(["first", 123, { nested: false }]);
});
it("should fall back to sync tasker when async tasker fails", async () => {
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("async tasker error")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-fallback-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const result = await tasker.dispatch("taskThatFails");
expect(mockAsyncTasker.taskThatFails).toHaveBeenCalledTimes(1);
expect(mockSyncTasker.taskThatFails).toHaveBeenCalledTimes(1);
expect(result).toBe("sync-fallback-result");
expect(mockLogger.error).toHaveBeenCalled();
expect(mockLogger.warn).toHaveBeenCalledWith("Trying again with SyncTasker for 'taskThatFails'.");
});
it("should pass params to sync tasker fallback when async fails", async () => {
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockRejectedValue(new Error("async failed")),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const syncReceivedParams: unknown[] = [];
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockImplementation((...args) => {
syncReceivedParams.push(...args);
return Promise.resolve("sync-result");
}),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const param1 = "fallback-test";
const param2 = 999;
const param3 = { nested: true };
await tasker.dispatch("taskWithParams", param1, param2, param3);
expect(syncReceivedParams).toEqual([param1, param2, param3]);
});
it("should throw error when both async and sync taskers fail", async () => {
const { Tasker } = await import("./Tasker");
const syncError = new Error("sync tasker also failed");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("async tasker error")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(syncError),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
await expect(tasker.dispatch("taskThatFails")).rejects.toThrow("sync tasker also failed");
expect(mockAsyncTasker.taskThatFails).toHaveBeenCalledTimes(1);
expect(mockSyncTasker.taskThatFails).toHaveBeenCalledTimes(1);
});
it("should log dispatch information with args", async () => {
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const args = ["test", 123, { nested: true }] as const;
await tasker.dispatch("taskWithParams", ...args);
expect(mockLogger.info).toHaveBeenCalledWith("Safely Dispatching task 'taskWithParams'", {
args: ["test", 123, { nested: true }],
});
});
});
describe("when async tasker is disabled (missing env vars)", () => {
beforeEach(() => {
delete process.env.TRIGGER_SECRET_KEY;
delete process.env.TRIGGER_API_URL;
});
it("should use sync tasker when TRIGGER_SECRET_KEY is missing", async () => {
vi.resetModules();
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const result = await tasker.dispatch("taskWithParams", "test", 1, { nested: true });
expect(result).toBe("sync-result");
expect(mockSyncTasker.taskWithParams).toHaveBeenCalledWith("test", 1, { nested: true });
});
it("should throw immediately when sync tasker fails (no fallback to itself)", async () => {
vi.resetModules();
const { Tasker } = await import("./Tasker");
const syncError = new Error("sync tasker failed");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(syncError),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
await expect(tasker.dispatch("taskThatFails")).rejects.toThrow("sync tasker failed");
expect(mockSyncTasker.taskThatFails).toHaveBeenCalledTimes(1);
expect(mockLogger.warn).not.toHaveBeenCalledWith(
expect.stringContaining("Trying again with SyncTasker")
);
});
it("should log warning about missing env vars", async () => {
vi.resetModules();
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(new Error("failed")),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
new TestTasker();
expect(mockLogger.info).toHaveBeenCalledWith(
"Missing env variables TRIGGER_SECRET_KEY or TRIGGER_API_URL, falling back to Sync tasker."
);
});
});
describe("error handling", () => {
beforeEach(() => {
process.env.TRIGGER_SECRET_KEY = "test-secret-key";
process.env.TRIGGER_API_URL = "https://test-trigger-api.com";
});
it("should log error details when async tasker fails", async () => {
const { Tasker } = await import("./Tasker");
const asyncError = new Error("async error message");
asyncError.name = "AsyncError";
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue(asyncError),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
await tasker.dispatch("taskThatFails");
expect(mockLogger.error).toHaveBeenCalledWith(
"AsyncTasker failed for 'taskThatFails'. (baseURL: https://test-trigger-api.com)",
expect.objectContaining({
name: "AsyncError",
message: "async error message",
})
);
});
it("should handle non-Error objects thrown by taskers", async () => {
const { Tasker } = await import("./Tasker");
const mockAsyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("async-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockRejectedValue("string error"),
};
const mockSyncTasker: MockTaskerInterface = {
taskWithParams: vi.fn().mockResolvedValue("sync-result"),
simpleTask: vi.fn().mockResolvedValue(undefined),
taskThatFails: vi.fn().mockResolvedValue("sync-result"),
};
const mockLogger = createMockLogger();
class TestTasker extends Tasker<MockTaskerInterface> {
constructor() {
super({
asyncTasker: mockAsyncTasker,
syncTasker: mockSyncTasker,
logger: mockLogger,
});
}
}
const tasker = new TestTasker();
const result = await tasker.dispatch("taskThatFails");
expect(result).toBe("sync-result");
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining("AsyncTasker failed"),
expect.objectContaining({ message: "string error" })
);
});
});
});