import { jest } from "@jest/globals"; const workerInstances = []; jest.unstable_mockModule("worker_threads", () => ({ Worker: class Worker { constructor(filename) { this.filename = filename; this.postMessage = jest.fn(); this.terminate = jest.fn().mockResolvedValue(undefined); this.listeners = {}; workerInstances.push(this); } on(event, handler) { this.listeners[event] = handler; } }, })); jest.unstable_mockModule("log4js", () => ({ default: { getLogger: () => ({ level: "info", debug: jest.fn(), error: jest.fn(), warn: jest.fn(), trace: jest.fn(), info: jest.fn(), }), }, })); jest.unstable_mockModule("../../config.js", () => ({ loadConfig: jest.fn(() => ({ server: { logLevel: "info", }, })), })); const onSchedulerEvent = jest.fn().mockResolvedValue(undefined); const invoiceMongooseModel = { scheduledProperties: [ { name: "dueAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, { name: "remindAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, ], onSchedulerEvent, }; const invoiceModelEntry = { type: "invoice", label: "Invoice", model: invoiceMongooseModel, }; const printerModelEntry = { type: "printer", label: "Printer", model: {}, }; jest.unstable_mockModule("../../database/schemas/models.js", () => ({ models: { INV: invoiceModelEntry, PRN: printerModelEntry, }, })); jest.unstable_mockModule("../../database/database.js", () => ({ listObjects: jest.fn(async () => []), getObject: jest.fn(async ({ id }) => ({ _id: id })), })); jest.unstable_mockModule("../../database/utils.js", () => ({ getFilter: jest.fn(async (filter) => filter), })); const updateManagerInstances = []; jest.unstable_mockModule("../../updates/updatemanager.js", () => ({ UpdateManager: class UpdateManager { constructor(socketClient) { this.socketClient = socketClient; this.subscribeToObjectNew = jest.fn().mockResolvedValue(undefined); this.subscribeToObjectDelete = jest.fn().mockResolvedValue(undefined); this.subscribeToAllObjectUpdates = jest.fn().mockResolvedValue(undefined); this.removeAllListeners = jest.fn().mockResolvedValue(undefined); updateManagerInstances.push(this); } }, })); const { SchedulerManager } = await import("../schedulermanager.js"); const { Scheduler } = await import("../scheduler.js"); const { listObjects, getObject } = await import("../../database/database.js"); const { getFilter } = await import("../../database/utils.js"); const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); const createManager = () => new SchedulerManager( { name: "redis" }, { name: "mongo" }, { name: "nats" }, ); const dueAt = new Date("2026-09-06T12:00:00.000Z"); describe("SchedulerManager", () => { beforeEach(() => { jest.clearAllMocks(); workerInstances.length = 0; updateManagerInstances.length = 0; onSchedulerEvent.mockClear(); listObjects.mockResolvedValue([]); getObject.mockImplementation(async ({ id }) => ({ _id: id })); }); describe("start", () => { it("should start the worker, discover scheduled properties, subscribe, and seed schedulers", async () => { const object = { _id: "inv-1", dueAt, remindAt: null }; listObjects.mockResolvedValue([object]); const manager = createManager(); await manager.start(); expect(workerInstances).toHaveLength(1); expect(manager.scheduledProperties).toEqual([ { model: invoiceModelEntry, property: "dueAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, { model: invoiceModelEntry, property: "remindAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, ]); const updateManager = manager.updateManager; expect(updateManager.subscribeToObjectNew).toHaveBeenCalledTimes(1); expect(updateManager.subscribeToObjectDelete).toHaveBeenCalledTimes(1); expect(updateManager.subscribeToAllObjectUpdates).toHaveBeenCalledTimes(1); expect(updateManager.subscribeToObjectNew).toHaveBeenCalledWith( "invoice", { state: "due|sent|acknowledged" }, ); expect(getFilter).toHaveBeenCalledWith( { state: "due|sent|acknowledged", dueAt: "TODAY.." }, ["*"], true, invoiceMongooseModel, ); expect(manager.schedulers).toHaveLength(1); expect(manager.schedulers[0]).toMatchObject({ objectType: "invoice", object, property: "dueAt", value: dueAt, }); expect(workerInstances[0].postMessage).toHaveBeenCalledWith({ type: "add", scheduler: manager.schedulers[0].toPayload(), }); }); it("should skip objects that are missing a scheduled property value", async () => { listObjects.mockResolvedValue([{ _id: "inv-1" }]); const manager = createManager(); await manager.start(); expect(manager.schedulers).toHaveLength(0); }); }); describe("handleWorkerMessage", () => { it("should fire a known scheduler and remove it afterwards", async () => { const manager = createManager(); manager.startWorker(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.addScheduler(scheduler); manager.handleWorkerMessage({ type: "due", id: "scheduler-1" }); await flushPromises(); expect(onSchedulerEvent).toHaveBeenCalledWith( { _id: "inv-1" }, "dueAt", ); expect(manager.schedulers).toHaveLength(0); expect(workerInstances[0].postMessage).toHaveBeenCalledWith({ type: "remove", id: "scheduler-1", }); }); it("should ignore unknown schedulers and non-due messages", async () => { const manager = createManager(); manager.startWorker(); manager.handleWorkerMessage({ type: "due", id: "missing" }); manager.handleWorkerMessage({ type: "add", id: "scheduler-1" }); await flushPromises(); expect(onSchedulerEvent).not.toHaveBeenCalled(); }); it("should still remove a scheduler when firing fails", async () => { onSchedulerEvent.mockRejectedValueOnce(new Error("boom")); const manager = createManager(); manager.startWorker(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.addScheduler(scheduler); manager.handleWorkerMessage({ type: "due", id: "scheduler-1" }); await flushPromises(); expect(manager.schedulers).toHaveLength(0); }); }); describe("upsertScheduler", () => { it("should no-op when the object has no id", () => { const manager = createManager(); manager.startWorker(); manager.upsertScheduler({ objectType: "invoice", object: {}, property: "dueAt", value: dueAt, }); expect(manager.schedulers).toHaveLength(0); expect(workerInstances[0].postMessage).not.toHaveBeenCalled(); }); it("should remove an existing scheduler when the value is cleared", () => { const manager = createManager(); manager.startWorker(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.addScheduler(scheduler); workerInstances[0].postMessage.mockClear(); manager.upsertScheduler({ objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: null, }); expect(manager.schedulers).toHaveLength(0); expect(workerInstances[0].postMessage).toHaveBeenCalledWith({ type: "remove", id: "scheduler-1", }); }); it("should update an existing scheduler without notifying the worker when the time is unchanged", () => { const manager = createManager(); manager.startWorker(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1", name: "old" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.addScheduler(scheduler); workerInstances[0].postMessage.mockClear(); const nextObject = { _id: "inv-1", name: "new" }; manager.upsertScheduler({ objectType: "invoice", object: nextObject, property: "dueAt", value: dueAt.toISOString(), }); expect(manager.schedulers).toHaveLength(1); expect(manager.schedulers[0].object).toBe(nextObject); expect(workerInstances[0].postMessage).not.toHaveBeenCalled(); }); it("should notify the worker when an existing scheduler time changes", () => { const manager = createManager(); manager.startWorker(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.addScheduler(scheduler); workerInstances[0].postMessage.mockClear(); const nextValue = new Date("2026-09-08T12:00:00.000Z"); manager.upsertScheduler({ objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: nextValue, }); expect(manager.schedulers[0].value).toEqual(nextValue); expect(workerInstances[0].postMessage).toHaveBeenCalledWith({ type: "add", scheduler: manager.schedulers[0].toPayload(), }); }); }); describe("findScheduler and removeSchedulersForObject", () => { it("should match schedulers by type, property, and stringified object id", () => { const manager = createManager(); const scheduler = new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: 123 }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }); manager.schedulers.push(scheduler); expect(manager.findScheduler("invoice", "123", "dueAt")).toBe(scheduler); expect(manager.findScheduler("invoice", "123", "remindAt")).toBeUndefined(); }); it("should remove every scheduler for an object and ignore missing ids", () => { const manager = createManager(); manager.startWorker(); manager.addScheduler( new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }), ); manager.addScheduler( new Scheduler({ id: "scheduler-2", objectType: "invoice", object: { _id: "inv-1" }, property: "remindAt", value: dueAt, model: invoiceMongooseModel, }), ); manager.addScheduler( new Scheduler({ id: "scheduler-3", objectType: "invoice", object: { _id: "inv-2" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }), ); manager.removeSchedulersForObject("invoice", {}); expect(manager.schedulers).toHaveLength(3); manager.removeSchedulersForObject("invoice", { _id: "inv-1" }); expect(manager.schedulers.map((scheduler) => scheduler.id)).toEqual([ "scheduler-3", ]); }); }); describe("handleObjectEvent", () => { it("should sync schedulers from objectNew and objectUpdate events", async () => { const manager = createManager(); manager.startWorker(); manager.scheduledProperties = [ { model: invoiceModelEntry, property: "dueAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, ]; getObject.mockResolvedValue({ _id: "inv-1", dueAt }); manager.handleObjectEvent("objectNew", { objectType: "invoice", object: { _id: "inv-1" }, filter: { state: "due|sent|acknowledged" }, }); await flushPromises(); expect(getObject).toHaveBeenCalledWith({ model: invoiceMongooseModel, id: "inv-1", }); expect(manager.schedulers).toHaveLength(1); getObject.mockResolvedValue({ _id: "inv-1", dueAt: new Date("2026-09-09T12:00:00.000Z"), }); manager.handleObjectEvent("objectUpdate", { objectType: "invoice", _id: "inv-1", object: { dueAt: new Date("2026-09-09T12:00:00.000Z") }, filter: { state: "due|sent|acknowledged" }, }); await flushPromises(); expect(manager.schedulers[0].value).toEqual( new Date("2026-09-09T12:00:00.000Z"), ); }); it("should ignore events whose filter does not match a scheduled property", async () => { const manager = createManager(); manager.startWorker(); manager.scheduledProperties = [ { model: invoiceModelEntry, property: "dueAt", filter: { state: "due|sent|acknowledged" }, type: "dateTime", }, ]; manager.handleObjectEvent("objectNew", { objectType: "invoice", object: { _id: "inv-1" }, filter: { state: "draft" }, }); await flushPromises(); expect(manager.schedulers).toHaveLength(0); }); it("should remove schedulers on objectDelete", () => { const manager = createManager(); manager.startWorker(); manager.addScheduler( new Scheduler({ id: "scheduler-1", objectType: "invoice", object: { _id: "inv-1" }, property: "dueAt", value: dueAt, model: invoiceMongooseModel, }), ); manager.handleObjectEvent("objectDelete", { objectType: "invoice", object: { _id: "inv-1" }, }); expect(manager.schedulers).toHaveLength(0); }); }); describe("stop", () => { it("should remove listeners and terminate the worker", async () => { const manager = createManager(); await manager.start(); manager.updateSubscriptions.add("invoice:{}"); await manager.stop(); expect(manager.updateManager.removeAllListeners).toHaveBeenCalled(); expect(manager.updateSubscriptions.size).toBe(0); expect(workerInstances[0].postMessage).toHaveBeenCalledWith({ type: "stop", }); expect(workerInstances[0].terminate).toHaveBeenCalled(); expect(manager.worker).toBeNull(); }); }); });