From bca6615da965b28f1f224fef11f63a9e70076a30 Mon Sep 17 00:00:00 2001 From: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Date: Sun, 18 Jan 2026 13:06:43 +0200 Subject: [PATCH] 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> --- packages/lib/tasker/Tasker.test.ts | 490 +++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 packages/lib/tasker/Tasker.test.ts diff --git a/packages/lib/tasker/Tasker.test.ts b/packages/lib/tasker/Tasker.test.ts new file mode 100644 index 0000000000..bc71b1ca65 --- /dev/null +++ b/packages/lib/tasker/Tasker.test.ts @@ -0,0 +1,490 @@ +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; + simpleTask: () => Promise; + taskThatFails: () => Promise; +} + +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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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" }) + ); + }); + }); +});