import { mock, test, expect } from "bun:test"; import * as TE from "fp-ts/lib/TaskEither"; import type { Logger } from "../../src/util"; import { perform, type JobsTable, execute, formatJobResults, logExecutingJobs, type Job } from "../../src/canary/scheduler"; const getMocks = () => { const mockLogger: Logger = { log: mock(), error: mock() }; return { mockLogger }; }; const schedule = { every: 200, jitter: 100 }; test("logging", () => { const { mockLogger } = getMocks(); const jobs: Job[] = [ { id: "1", toString: () => "Job 1", execute: mock(), schedule, maxRetries: 3 }, { id: "2", toString: () => "Job 2", execute: mock(), schedule, maxRetries: 3 } ]; const now = new Date("2023-01-01T00:00:00Z"); logExecutingJobs(jobs, now, mockLogger); expect(mockLogger.log).toHaveBeenCalledWith("Executing Job 1|Job 2 at Sun, 01 Jan 2023 00:00:00 GMT"); }); test("should separate jobs into successful and failed executions", async () => { const job1: Job = { id: "1", toString: () => "Job 1", execute: mock(() => TE.right("Result 1") as any), schedule, maxRetries: 3 }; const job2: Job = { id: "2", toString: () => "Job 2", execute: mock(() => TE.left(new Error("Failure 2")) as any), schedule, maxRetries: 3 }; const jobs: Job[] = [job1, job2]; const result = await execute(jobs)(); expect(result.left).toEqual([[job2, new Error("Failure 2")]]); expect(result.right).toEqual([[job1, "Result 1"]]); }); test("should format job results correctly", () => { const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 1 }]]); const left = [ [{ id: "1", toString: () => "Job 1", execute: mock(), schedule: {}, maxRetries: 3 }, new Error("Error 1")] ]; const right = [[{ id: "2", toString: () => "Job 2", execute: mock(), schedule: {}, maxRetries: 3 }, "Success 2"]]; const result = formatJobResults(jobsTable, { left, right } as any); expect(result).toContain("Job 1 | 1 / 3 | (retry) :/ | Error 1"); expect(result).toContain("Job 2 | (success) :) | Success 2"); }); test("should update jobsTable and lastPingAck correctly", async () => { const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 1 }]]); const jobs: Job[] = [ { id: "1", toString: () => "Job 1", execute: mock(() => TE.right("Result 1") as any), schedule, maxRetries: 3 } ]; const publishers: any = []; const lastPingAck = new Date("2023-01-01T00:00:00Z"); const result = await perform(jobsTable, jobs, publishers, lastPingAck)(); expect(result.lastPingAck).not.toEqual(lastPingAck); expect(result.jobsTable.get("1")).toEqual({ retries: 0, scheduled: expect.any(Date) }); }); test("should update a job with retry count on failure", async () => { // Create a mock job that fails the first time but succeeds the second time const job1: Job = { id: "1", toString: () => "Job 1", execute: mock(() => TE.left(new Error("Error 1")) as any), schedule, maxRetries: 2 }; const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 0 }]]); const jobs: Job[] = [job1]; const publishers: any = []; const lastPingAck = new Date("2023-01-01T00:00:00Z"); const result = await perform(jobsTable, jobs, publishers, lastPingAck, 24 * 60 * 60 * 1000)(); // Assert the job was retried once and then succeeded expect(job1.execute).toHaveBeenCalled(); // Check the jobsTable for the updated state expect(result.jobsTable.get("1")).toEqual({ retries: 1, scheduled: expect.any(Date) }); }); test("should reschedule a job that hits max retries", async () => { // Create a mock job that fails the first time but succeeds the second time const job1: Job = { id: "1", toString: () => "Job 1", execute: mock().mockReturnValue(TE.left(new Error("Error 1"))), schedule, maxRetries: 4 }; const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 4 }]]); const jobs: Job[] = [job1]; const publishers: any = [mock().mockReturnValue(TE.right(200))]; const lastPingAck = new Date("2023-01-01T00:00:00Z"); const now = Date.now(); const result = await perform(jobsTable, jobs, publishers, lastPingAck, 24 * 60 * 60 * 1000)(); const delta = Date.now() - now; // Assert the job was retried once and then fail expect(job1.execute).toHaveBeenCalled(); // Check the jobsTable for the updated state const { retries, scheduled } = result.jobsTable.get("1")!; expect(retries).toEqual(0); expect(publishers[0]).toHaveBeenCalled(); expect(scheduled.getTime()).toBeGreaterThan(now - delta + schedule.every); expect(scheduled.getTime()).toBeLessThan(now + delta + schedule.every + schedule.jitter); }); test("should not publish only successes when should not ack", async () => { // Create a mock job that fails the first time but succeeds the second time const job1: Job = { id: "1", toString: () => "Job 1", execute: mock().mockReturnValue(TE.right(new Error("Error 1"))), schedule, maxRetries: 4 }; const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 0 }]]); const jobs: Job[] = [job1]; const publishers: any = [mock().mockReturnValue(TE.right(200))]; const lastPingAck = new Date(); await perform(jobsTable, jobs, publishers, lastPingAck, 24 * 60 * 60 * 1000)(); expect(job1.execute).toHaveBeenCalled(); expect(publishers[0]).toHaveBeenCalledTimes(0); }); test("should publish when should ack", async () => { // Create a mock job that fails the first time but succeeds the second time const job1: Job = { id: "1", toString: () => "Job 1", execute: mock().mockReturnValue(TE.right(new Error("Error 1"))), schedule, maxRetries: 4 }; const jobsTable: JobsTable = new Map([["1", { scheduled: new Date("2023-01-01T00:00:00Z"), retries: 0 }]]); const jobs: Job[] = [job1]; const publishers: any = [mock().mockReturnValue(TE.right(200))]; const lastPingAck = new Date("2023-01-01T00:00:00Z"); await perform(jobsTable, jobs, publishers, lastPingAck, 24 * 60 * 60 * 1000)(); expect(job1.execute).toHaveBeenCalled(); expect(publishers[0]).toHaveBeenCalled(); });