- 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>
491 lines
16 KiB
TypeScript
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" })
|
|
);
|
|
});
|
|
});
|
|
});
|