diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts index c53f270a7..45db5b3f7 100644 --- a/server/test/helpers/InMemoryMonitorsRepository.ts +++ b/server/test/helpers/InMemoryMonitorsRepository.ts @@ -40,6 +40,11 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository { return this.monitors.filter((m) => monitorIds.includes(m.id)).map((m) => ({ ...m })); } + async findByIdLean(monitorId: string): Promise { + const monitor = this.monitors.find((m) => m.id === monitorId); + return monitor ? { ...monitor } : null; + } + async findByIdsWithChecks(monitorIds: string[], _checksCount?: number): Promise { return this.findByIds(monitorIds); } diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index baf7eeee3..5711f5aa9 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -1,7 +1,5 @@ import { jest } from "@jest/globals"; -import { WorkerHelper } from "../../src/worker/worker.helper.ts"; import { MonitorStatusPolicy } from "../../src/worker/worker.monitor-status-policy.ts"; -import { CheckPipeline, GeoChecksPipeline } from "../../src/worker/worker.check-pipeline.ts"; import { CheckProducer } from "../../src/worker/worker.check-producer.ts"; import { CheckEvaluator } from "../../src/worker/worker.check-evaluator.ts"; import { NotificationReactor } from "../../src/worker/reactors/reactor.notification.ts"; @@ -71,20 +69,6 @@ const createStubMonitorStatsRepo = () => ({ deleteByMonitorIdsNotIn: jest.fn(), }); -const createStubChecksRepo = () => ({ - create: jest.fn(), - createChecks: jest.fn(), - findByMonitorId: jest.fn(), - findByTeamId: jest.fn(), - findLatestByMonitorIds: jest.fn(), - findByDateRangeAndMonitorId: jest.fn(), - findSummaryByTeamId: jest.fn(), - deleteByMonitorId: jest.fn(), - deleteByTeamId: jest.fn(), - deleteByMonitorIdsNotIn: jest.fn(), - deleteOlderThan: jest.fn(), -}); - export interface HeartbeatTestHarness { monitorsRepo: InMemoryMonitorsRepository; incidentsRepo: InMemoryIncidentsRepository; @@ -108,13 +92,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { const logger = createMockLogger() as any; const bufferStub = { addToBuffer: jest.fn(), addGeoCheckToBuffer: jest.fn(), scheduleNextFlush: jest.fn() }; - const statusService = new StatusService( - logger, - bufferStub as any, - monitorsRepo as any, - createStubMonitorStatsRepo() as any, - createStubChecksRepo() as any - ); + const statusService = new StatusService(logger, monitorsRepo as any, createStubMonitorStatsRepo() as any); const messageBuilder = { extractThresholdBreaches: jest.fn().mockReturnValue([]) }; const incidentService = new IncidentService(logger, incidentsRepo, monitorsRepo as any, { findById: jest.fn() } as any, messageBuilder as any); @@ -149,7 +127,6 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { }; const maintenanceWindowsRepo = { findByMonitorId: jest.fn().mockResolvedValue([]) }; - const geoChecksService = { buildGeoCheck: jest.fn() }; const checkProducer = new CheckProducer( monitorsRepo as any, @@ -160,28 +137,21 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { logger ); const checkEvaluator = new CheckEvaluator(statusService as any, new MonitorStatusPolicy()); - const checkPipeline = new CheckPipeline(checkProducer, checkEvaluator); - const geoCheckPipeline = new GeoChecksPipeline(maintenanceWindowsRepo as any, geoChecksService as any, bufferStub as any, logger); const notificationReactor = new NotificationReactor(notificationsService as any); const incidentReactor = new IncidentReactor(incidentService as any); const reactorDispatcher = new ReactorDispatcher(logger, [notificationReactor, incidentReactor]); - const helper = new WorkerHelper( - logger, - checkService as any, - { getDBSettings: jest.fn() } as any, - monitorsRepo as any, - { deleteByMonitorIdsNotIn: jest.fn() } as any, - { findAllTeamIds: jest.fn() } as any, - createStubMonitorStatsRepo() as any, - createStubChecksRepo() as any, - incidentsRepo as any, - { deleteByMonitorIdsNotIn: jest.fn() } as any, - reactorDispatcher, - checkPipeline, - geoCheckPipeline - ); + // Mirror the production check→evaluate flow: read the monitor fresh each cycle (so the + // accumulated statusWindow is visible), produce a check, then evaluate against that monitor. + const heartbeatJob = async (seedMonitor: Monitor) => { + const monitor = await monitorsRepo.findByIdLean(seedMonitor.id); + if (!monitor) return; + const result = await checkProducer.produce(monitor); + if (!result) return; // skipped (e.g. maintenance window) + const evaluation = await checkEvaluator.evaluate(result.status, result.check, monitor); + await reactorDispatcher.dispatch(evaluation); + }; return { monitorsRepo, @@ -193,7 +163,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { bufferStub, maintenanceWindowsRepo, messageBuilder, - heartbeatJob: helper.getHeartbeatJob(), + heartbeatJob, setNextResponse, setNextResponseFull, }; diff --git a/server/test/unit/services/checkPipeline.test.ts b/server/test/unit/services/checkPipeline.test.ts index 9b1fffd8b..008e879fa 100644 --- a/server/test/unit/services/checkPipeline.test.ts +++ b/server/test/unit/services/checkPipeline.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it, jest } from "@jest/globals"; -import { CheckPipeline, GeoChecksPipeline } from "../../../src/worker/worker.check-pipeline.ts"; -import { CheckProducer } from "../../../src/worker/worker.check-producer.ts"; -import { CheckEvaluator } from "../../../src/worker/worker.check-evaluator.ts"; -import { MonitorStatusPolicy } from "../../../src/worker/worker.monitor-status-policy.ts"; +import { GeoChecksPipeline } from "../../../src/worker/worker.check-pipeline.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; @@ -23,114 +20,6 @@ const activeWindow = () => { return { active: true, start: new Date(now - 1000).toISOString(), end: new Date(now + 1000).toISOString(), repeat: 0 }; }; -// ── CheckPipeline ──────────────────────────────────────────────────────────── - -const createCheckPipeline = (overrides?: Record) => { - const defaults = { - logger: createMockLogger(), - networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, - statusService: { - updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1", status: "up" }, statusChanged: false, prevStatus: "up", code: 200 }), - }, - checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) }, - buffer: { addToBuffer: jest.fn() }, - monitorsRepository: { updateById: jest.fn().mockResolvedValue({}) }, - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) }, - monitorStatusPolicy: new MonitorStatusPolicy(), - ...overrides, - }; - const checkProducer = new CheckProducer( - defaults.monitorsRepository as any, - defaults.maintenanceWindowsRepository as any, - defaults.checkService as any, - defaults.networkService as any, - defaults.buffer as any, - defaults.logger as any - ); - const checkEvaluator = new CheckEvaluator(defaults.statusService as any, defaults.monitorStatusPolicy as any); - const pipeline = new CheckPipeline(checkProducer, checkEvaluator); - return { pipeline, defaults }; -}; - -describe("CheckPipeline", () => { - it("throws when monitor id is missing", async () => { - const { pipeline } = createCheckPipeline(); - await expect(pipeline.run({} as Monitor)).rejects.toThrow("No monitor id"); - }); - - it("skips and sets maintenance status when in maintenance window", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, - }); - - const result = await pipeline.run(makeMonitor({ status: "up" })); - - expect(result).toBeNull(); - expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); - expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance" }); - }); - - it("does not re-write status when already in maintenance", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, - }); - - await pipeline.run(makeMonitor({ status: "maintenance" })); - - expect(defaults.monitorsRepository.updateById).not.toHaveBeenCalled(); - }); - - it("throws when network response is null", async () => { - const { pipeline } = createCheckPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(null) }, - }); - await expect(pipeline.run(makeMonitor())).rejects.toThrow("No network response"); - }); - - it("returns null and warns when toCheck returns null", async () => { - const { pipeline, defaults } = createCheckPipeline({ - checkService: { toCheck: jest.fn().mockReturnValue(null) }, - }); - - const result = await pipeline.run(makeMonitor()); - - expect(result).toBeNull(); - expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("No check could be built") })); - expect(defaults.statusService.updateMonitorStatus).not.toHaveBeenCalled(); - }); - - it("buffers the check, updates status, and returns an evaluation", async () => { - const status = { monitorId: "m1", status: false, code: 500, message: "Error" }; - const statusChange = { monitor: { id: "m1", status: "down" }, statusChanged: true, prevStatus: "up", code: 500 }; - const { pipeline, defaults } = createCheckPipeline({ - networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, - statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, - }); - - const result = await pipeline.run(makeMonitor()); - - expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith({ id: "check-1" }); - expect(defaults.statusService.updateMonitorStatus).toHaveBeenCalled(); - expect(result).toMatchObject({ - monitor: statusChange.monitor, - status, - check: { id: "check-1" }, - statusChange, - decision: expect.objectContaining({ shouldCreateIncident: true, shouldSendNotification: true, incidentReason: "status_down" }), - }); - }); - - it("proceeds with the check when no maintenance window is active", async () => { - const { pipeline, defaults } = createCheckPipeline({ - maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([{ ...activeWindow(), active: false }]) }, - }); - - await pipeline.run(makeMonitor()); - - expect(defaults.networkService.requestStatus).toHaveBeenCalled(); - }); -}); - // ── GeoChecksPipeline ────────────────────────────────────────────────────────── const createGeoPipeline = (overrides?: Record) => { diff --git a/server/test/unit/services/checkProducer.test.ts b/server/test/unit/services/checkProducer.test.ts new file mode 100644 index 000000000..bf93c8ea2 --- /dev/null +++ b/server/test/unit/services/checkProducer.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { CheckProducer } from "../../../src/worker/worker.check-producer.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import { createMockLogger } from "../../helpers/createMockLogger.ts"; + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "m1", + teamId: "team", + type: "http", + interval: 60000, + status: "up", + ...overrides, + }) as Monitor; + +// A maintenance window straddling "now" — active per isWindowActive. +const activeWindow = () => { + const now = Date.now(); + return { active: true, start: new Date(now - 1000).toISOString(), end: new Date(now + 1000).toISOString(), repeat: 0 }; +}; + +const createProducer = (overrides?: Record) => { + const defaults = { + logger: createMockLogger(), + monitorsRepository: { updateById: jest.fn().mockResolvedValue({}) }, + maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) }, + checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) }, + networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, + buffer: { addToBuffer: jest.fn() }, + ...overrides, + }; + const producer = new CheckProducer( + defaults.monitorsRepository as any, + defaults.maintenanceWindowsRepository as any, + defaults.checkService as any, + defaults.networkService as any, + defaults.buffer as any, + defaults.logger as any + ); + return { producer, defaults }; +}; + +describe("CheckProducer", () => { + it("throws when monitor id is missing", async () => { + const { producer } = createProducer(); + await expect(producer.produce({} as Monitor)).rejects.toThrow("No monitor id"); + }); + + // ── maintenance gate ────────────────────────────────────────────────────── + + it("skips the check and flips status to 'maintenance' when in a maintenance window", async () => { + const { producer, defaults } = createProducer({ + maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, + }); + + const result = await producer.produce(makeMonitor({ status: "up" })); + + expect(result).toBeNull(); + expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance" }); + expect(defaults.networkService.requestStatus).not.toHaveBeenCalled(); + expect(defaults.buffer.addToBuffer).not.toHaveBeenCalled(); + }); + + it("does not re-write status when already in maintenance", async () => { + const { producer, defaults } = createProducer({ + maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) }, + }); + + const result = await producer.produce(makeMonitor({ status: "maintenance" })); + + expect(result).toBeNull(); + expect(defaults.monitorsRepository.updateById).not.toHaveBeenCalled(); + }); + + it("proceeds with the check when the maintenance window is inactive", async () => { + const { producer, defaults } = createProducer({ + maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([{ ...activeWindow(), active: false }]) }, + }); + + await producer.produce(makeMonitor()); + + expect(defaults.networkService.requestStatus).toHaveBeenCalled(); + }); + + // ── acquire / record ────────────────────────────────────────────────────── + + it("throws when the network response is null", async () => { + const { producer } = createProducer({ + networkService: { requestStatus: jest.fn().mockResolvedValue(null) }, + }); + + await expect(producer.produce(makeMonitor())).rejects.toThrow("No network response"); + }); + + it("returns null, warns, and does not buffer when toCheck yields nothing", async () => { + const { producer, defaults } = createProducer({ + checkService: { toCheck: jest.fn().mockReturnValue(null) }, + }); + + const result = await producer.produce(makeMonitor()); + + expect(result).toBeNull(); + expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("No check could be built") })); + expect(defaults.buffer.addToBuffer).not.toHaveBeenCalled(); + }); + + it("buffers the built check and returns the status and check on success", async () => { + const status = { monitorId: "m1", status: false, code: 500, message: "Error" }; + const check = { id: "check-1" }; + const { producer, defaults } = createProducer({ + networkService: { requestStatus: jest.fn().mockResolvedValue(status) }, + checkService: { toCheck: jest.fn().mockReturnValue(check) }, + }); + + const result = await producer.produce(makeMonitor()); + + expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith(check); + expect(result).toEqual({ status, check }); + }); +}); diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts index e60f82a95..1769ed573 100644 --- a/server/test/unit/services/dbQueueWorker.test.ts +++ b/server/test/unit/services/dbQueueWorker.test.ts @@ -80,6 +80,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses }; const monitorsRepository = { findByIds: jest.fn().mockResolvedValue([makeMonitor()]), + findByIdLean: jest.fn().mockResolvedValue(makeMonitor()), findAll: jest.fn().mockResolvedValue([]), updateById: jest.fn().mockResolvedValue({}), }; @@ -94,7 +95,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses produce: jest.fn().mockResolvedValue({ status: { status: "up" }, check: { id: "c1" } }), }; const checkEvaluator = { - evaluate: jest.fn().mockResolvedValue({ monitor: makeMonitor(), decision: {} }), + evaluate: jest.fn().mockResolvedValue({ monitor: makeMonitor(), statusChange: { monitor: makeMonitor() }, decision: {} }), }; const geoCheckPipeline = { run: jest.fn().mockResolvedValue(null) }; const dispatcher = { dispatch: jest.fn().mockResolvedValue(undefined) }; @@ -268,7 +269,7 @@ describe("DBQueueWorker", () => { const job = makeJob({ type: "check", refId: "m1", intervalMs: 60000 }); const { mocks } = await start({ mocks: { jobsRepository: { ...createWorker().mocks.jobsRepository, claimDueBatch: claimOnce(job) } } }); - expect(mocks.monitorsRepository.findByIds).toHaveBeenCalledWith(["m1"]); + expect(mocks.monitorsRepository.findByIdLean).toHaveBeenCalledWith("m1"); expect(mocks.checkProducer.produce).toHaveBeenCalled(); // The check stage only produces; the buffer arms evaluate once the check is durably stored expect(mocks.jobsRepository.upsertEvaluate).not.toHaveBeenCalled(); @@ -307,6 +308,35 @@ describe("DBQueueWorker", () => { expect(mocks.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { lastEvaluatedAt: 12345 }); }); + it("evaluate job reads the monitor once and threads the post-write monitor across the backlog", async () => { + const job = makeJob({ id: "evaluate:m1", type: "evaluate", intervalMs: null }); + const initialMonitor = makeMonitor(); + const postWriteMonitor = makeMonitor({ status: "down" }); // what updateStatusWindowAndChecks would return after check c1 + const monitorsRepository = { + findByIdLean: jest.fn().mockResolvedValue(initialMonitor), + updateById: jest.fn().mockResolvedValue({}), + }; + const checksRepository = { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([{ id: "c1" }, { id: "c2" }]) }; + const checkEvaluator = { + evaluate: jest.fn().mockResolvedValue({ monitor: postWriteMonitor, statusChange: { monitor: postWriteMonitor }, decision: {} }), + }; + const { mocks } = await start({ + mocks: { + jobsRepository: { ...createWorker().mocks.jobsRepository, claimDueBatch: claimOnce(job) }, + monitorsRepository, + checksRepository, + checkEvaluator, + }, + }); + + // The monitor is read once for the whole backlog, not once per check + expect(mocks.monitorsRepository.findByIdLean).toHaveBeenCalledTimes(1); + expect(mocks.checkEvaluator.evaluate).toHaveBeenCalledTimes(2); + // First check evaluates against the freshly-read monitor; the second against the post-write monitor from the first + expect(mocks.checkEvaluator.evaluate.mock.calls[0][2]).toBe(initialMonitor); + expect(mocks.checkEvaluator.evaluate.mock.calls[1][2]).toBe(postWriteMonitor); + }); + it("cleanup-orphaned job runs the helper's cleanup function", async () => { const job = makeJob({ id: "cleanup-orphaned", type: "cleanup-orphaned", refId: null, intervalMs: 86400000 }); const cleanupFn = jest.fn().mockResolvedValue(undefined); diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 2bd5fa74c..f011c183c 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -7,21 +7,15 @@ import type { Check } from "../../../src/domain/checks/check.type.ts"; import type { MonitorStatusResponse, HardwareStatusPayload } from "../../../src/types/network.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { IMonitorStatsRepository } from "../../../src/domain/monitor-stats/monitor-stats.repository.interface.ts"; -import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts"; -import type { IBufferService } from "../../../src/service/bufferService.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── -const createBuffer = (): jest.Mocked> => ({ - addToBuffer: jest.fn(), -}); - const createMonitorsRepo = () => { const findById = jest.fn(); const updateStatusWindowAndChecks = jest .fn() .mockImplementation( - ( + async ( _id: unknown, _tid: unknown, status: boolean, @@ -30,27 +24,23 @@ const createMonitorsRepo = () => { maxRecentChecks: number, statusPatch?: Partial ) => { - // Grab the monitor that findById was configured to resolve with - const monitor = findById.mock.results.at(-1)?.value; - if (monitor && typeof monitor.then === "function") { - return monitor.then((m: any) => { - m.statusWindow = m.statusWindow || []; - m.statusWindow.push(status); - while (m.statusWindow.length > windowSize) { - m.statusWindow.shift(); - } - m.recentChecks = m.recentChecks || []; - m.recentChecks.push(checkSnapshot); - while (m.recentChecks.length > maxRecentChecks) { - m.recentChecks.shift(); - } - if (statusPatch) { - Object.assign(m, statusPatch); - } - return { ...m }; - }); + // updateMonitorStatus no longer reads the monitor itself — tests seed it via + // findById.mockResolvedValue, so the mock resolves it here as the post-write monitor. + const m: any = await findById(); + m.statusWindow = m.statusWindow || []; + m.statusWindow.push(status); + while (m.statusWindow.length > windowSize) { + m.statusWindow.shift(); } - return Promise.reject(new Error("findById not mocked")); + m.recentChecks = m.recentChecks || []; + m.recentChecks.push(checkSnapshot); + while (m.recentChecks.length > maxRecentChecks) { + m.recentChecks.shift(); + } + if (statusPatch) { + Object.assign(m, statusPatch); + } + return { ...m }; } ); return { @@ -67,23 +57,17 @@ const createMonitorStatsRepo = () => updateByMonitorId: jest.fn(), }) as unknown as jest.Mocked; -const createChecksRepo = () => ({}) as unknown as jest.Mocked; - const createService = (overrides?: { logger?: ReturnType; - buffer?: ReturnType; monitorsRepository?: ReturnType; monitorStatsRepository?: ReturnType; - checksRepository?: ReturnType; }) => { const logger = overrides?.logger ?? createMockLogger(); - const buffer = overrides?.buffer ?? createBuffer(); const monitorsRepository = overrides?.monitorsRepository ?? createMonitorsRepo(); const monitorStatsRepository = overrides?.monitorStatsRepository ?? createMonitorStatsRepo(); - const checksRepository = overrides?.checksRepository ?? createChecksRepo(); - const service = new StatusService(logger as any, buffer as any, monitorsRepository, monitorStatsRepository, checksRepository); - return { service, logger, buffer, monitorsRepository, monitorStatsRepository, checksRepository }; + const service = new StatusService(logger as any, monitorsRepository, monitorStatsRepository); + return { service, logger, monitorsRepository, monitorStatsRepository }; }; const makeMonitor = (overrides?: Partial): Monitor => @@ -264,19 +248,31 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck()); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor); expect(result.statusChanged).toBe(false); expect(result.prevStatus).toBe("up"); }); + it("uses the prefetched monitor and skips findById when one is passed", async () => { + const monitor = makeMonitor({ statusWindow: [], statusWindowSize: 5, status: "up" }); + const { service, monitorsRepository } = createService(); + (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockResolvedValue(monitor); + + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor); + + expect(monitorsRepository.findById).not.toHaveBeenCalled(); + expect(result.prevStatus).toBe("up"); + expect(result.monitor).toBe(monitor); + }); + it("pushes to statusWindow and trims to statusWindowSize", async () => { const monitor = makeMonitor({ statusWindow: [true, true, true, true, true], statusWindowSize: 5 }); const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); // Atomic push should have been called with the correct status and window size expect(monitorsRepository.updateStatusWindowAndChecks).toHaveBeenCalledWith( @@ -297,7 +293,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - await service.updateMonitorStatus(makeStatusResponse(), makeCheck({ id: "new-check" })); + await service.updateMonitorStatus(makeStatusResponse(), makeCheck({ id: "new-check" }), monitor); expect(monitor.recentChecks).toHaveLength(MAX_RECENT_CHECKS); expect(monitor.recentChecks[MAX_RECENT_CHECKS - 1]).toEqual(expect.objectContaining({ id: "new-check" })); @@ -315,7 +311,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -333,7 +329,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("up"); @@ -350,7 +346,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck()); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -370,7 +366,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -387,7 +383,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -399,7 +395,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - await service.updateMonitorStatus(makeStatusResponse(), makeCheck()); + await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor); expect(monitor.statusWindow).toEqual([true]); }); @@ -410,23 +406,25 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - await service.updateMonitorStatus(makeStatusResponse(), makeCheck()); + await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor); expect(monitor.recentChecks).toHaveLength(1); }); it("throws AppError when repository throws", async () => { + const monitor = makeMonitor(); const { service, monitorsRepository } = createService(); - (monitorsRepository.findById as jest.Mock).mockRejectedValue(new Error("db error")); + (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue(new Error("db error")); - await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck())).rejects.toThrow("Failed to update monitor"); + await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor)).rejects.toThrow("Failed to update monitor"); }); it("throws AppError with 'Unknown error' for non-Error exceptions", async () => { + const monitor = makeMonitor(); const { service, monitorsRepository } = createService(); - (monitorsRepository.findById as jest.Mock).mockRejectedValue("string error"); + (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue("string error"); - await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck())).rejects.toThrow("Unknown error"); + await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor)).rejects.toThrow("Unknown error"); }); it("keeps status 'down' on a single successful check that leaves failure rate at threshold (regression: #3438 mirror)", async () => { @@ -444,7 +442,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -462,7 +460,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -484,7 +482,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); }); @@ -502,7 +500,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.monitor.status).toBe("down"); }); @@ -518,7 +516,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -535,7 +533,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -550,7 +548,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -567,7 +565,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true })); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -580,7 +578,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue(new Error("stats db down")); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck()); + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor); expect(result.monitor.status).toBe("up"); expect(logger.warn).toHaveBeenCalledWith( @@ -598,7 +596,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse({ code: 201 }), makeCheck()); + const result = await service.updateMonitorStatus(makeStatusResponse({ code: 201 }), makeCheck(), monitor); expect(result.code).toBe(201); expect(result.timestamp).toBeGreaterThan(0); @@ -639,7 +637,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.5 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.cpu).toBe(true); expect(result.thresholdBreaches?.memory).toBe(false); @@ -656,7 +654,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.5 }, memory: { usage_percent: 0.9 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.memory).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -673,7 +671,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [{ usage_percent: 0.95 }], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.disk).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -690,7 +688,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1, temperature: [90] }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.temp).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -712,7 +710,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1, temperature: [30] }, memory: { usage_percent: 0.1 }, disk: [{ usage_percent: 0.1 }], host: {} }, } as any); - await service.updateMonitorStatus(response, makeCheck()); + await service.updateMonitorStatus(response, makeCheck(), monitor); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; expect(patch.cpuAlertCounter).toBe(5); @@ -731,7 +729,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.monitor.status).toBe("breached"); expect(result.statusChanged).toBe(false); @@ -746,7 +744,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1, temperature: [30] }, memory: { usage_percent: 0.1 }, disk: [{ usage_percent: 0.1 }], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("up"); @@ -769,7 +767,7 @@ describe("StatusService", () => { status: false, data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck({ status: false })); + const result = await service.updateMonitorStatus(response, makeCheck({ status: false }), monitor); expect(result.monitor.status).toBe("down"); }); @@ -781,7 +779,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: {}, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.cpu).toBe(false); }); @@ -793,7 +791,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: {}, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.memory).toBe(false); }); @@ -807,7 +805,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1, temperature: [] }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.temp).toBe(false); }); @@ -821,7 +819,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.temp).toBe(false); }); @@ -833,7 +831,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse(undefined as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches).toBeUndefined(); }); @@ -845,7 +843,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({} as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches).toBeUndefined(); }); @@ -860,7 +858,7 @@ describe("StatusService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); - const result = await service.updateMonitorStatus(makeStatusResponse(), makeCheck()); + const result = await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor); expect(result.thresholdBreaches).toBeUndefined(); }); @@ -874,7 +872,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [null as any], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -888,7 +886,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [{ device: "/dev/sda" }], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -902,7 +900,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: undefined, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.cpu).toBe(false); expect(result.thresholdBreaches?.temp).toBe(false); @@ -917,7 +915,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: undefined, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.memory).toBe(false); }); @@ -931,7 +929,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: undefined, host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -945,7 +943,7 @@ describe("StatusService", () => { (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck()); + const result = await service.updateMonitorStatus(response, makeCheck(), monitor); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; expect(patch.cpuAlertCounter).toBe(2); @@ -971,7 +969,7 @@ describe("StatusService", () => { status: false, data: { cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, } as any); - const result = await service.updateMonitorStatus(response, makeCheck({ status: false })); + const result = await service.updateMonitorStatus(response, makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -991,7 +989,7 @@ describe("StatusService", () => { const response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9, temperature: [90] }, memory: { usage_percent: 0.9 }, disk: [{ usage_percent: 0.95 }], host: {} }, } as any); - await service.updateMonitorStatus(response, makeCheck()); + await service.updateMonitorStatus(response, makeCheck(), monitor); expect(monitor.cpuAlertCounter).toBe(0); expect(monitor.memoryAlertCounter).toBe(0); diff --git a/server/test/unit/services/workerHelper.test.ts b/server/test/unit/services/workerHelper.test.ts index 27f06ab6c..96b45d3b3 100644 --- a/server/test/unit/services/workerHelper.test.ts +++ b/server/test/unit/services/workerHelper.test.ts @@ -1,14 +1,11 @@ import { describe, expect, it, jest } from "@jest/globals"; import { WorkerHelper } from "../../../src/worker/worker.helper.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── // -// WorkerHelper is now thin glue: it wires the scheduler's job functions to the -// CheckPipeline / GeoChecksPipeline (acquire→record→decide) and the -// ReactorDispatcher (react), and owns the cleanup jobs. The pipeline and -// dispatcher internals are tested in checkPipeline.test.ts / reactorDispatcher.test.ts; +// WorkerHelper is now thin glue: it wires the geo job to the GeoChecksPipeline and +// owns the cleanup jobs. The pipeline internals are tested in checkPipeline.test.ts; // here we test only the glue + cleanup jobs. const createHelper = (overrides?: Record) => { @@ -42,10 +39,6 @@ const createHelper = (overrides?: Record) => { deleteOlderThan: jest.fn().mockResolvedValue(0), }; - const checkPipeline = { run: jest.fn().mockResolvedValue(null) }; - const geoCheckPipeline = { run: jest.fn().mockResolvedValue(null) }; - const reactorDispatcher = { dispatch: jest.fn().mockResolvedValue(undefined) }; - const defaults = { logger: createMockLogger(), checkService, @@ -57,9 +50,6 @@ const createHelper = (overrides?: Record) => { checksRepository, incidentsRepository, geoChecksRepository, - reactorDispatcher, - checkPipeline, - geoCheckPipeline, ...overrides, }; @@ -73,26 +63,11 @@ const createHelper = (overrides?: Record) => { defaults.monitorStatsRepository as any, defaults.checksRepository as any, defaults.incidentsRepository as any, - defaults.geoChecksRepository as any, - defaults.reactorDispatcher as any, - defaults.checkPipeline as any, - defaults.geoCheckPipeline as any + defaults.geoChecksRepository as any ); return { helper, defaults }; }; -const makeMonitor = (overrides?: Partial): Monitor => - ({ - id: "m1", - teamId: "team", - type: "http", - interval: 60000, - status: "up", - geoCheckEnabled: false, - geoCheckLocations: [], - ...overrides, - }) as Monitor; - // ── Tests ──────────────────────────────────────────────────────────────────── describe("WorkerHelper", () => { @@ -103,68 +78,6 @@ describe("WorkerHelper", () => { }); }); - // ── getHeartbeatJob (glue) ──────────────────────────────────────────────── - - describe("getHeartbeatJob", () => { - it("runs the pipeline and dispatches the evaluation to reactors", async () => { - const evaluation = { monitor: { id: "m1" }, decision: {} }; - const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockResolvedValue(evaluation) } }); - - await helper.getHeartbeatJob()(makeMonitor()); - - expect(defaults.checkPipeline.run).toHaveBeenCalled(); - expect(defaults.reactorDispatcher.dispatch).toHaveBeenCalledWith(evaluation); - }); - - it("does not dispatch when the pipeline returns null (skipped)", async () => { - const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockResolvedValue(null) } }); - - await helper.getHeartbeatJob()(makeMonitor()); - - expect(defaults.reactorDispatcher.dispatch).not.toHaveBeenCalled(); - }); - - it("logs and rethrows when the pipeline throws", async () => { - const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockRejectedValue(new Error("boom")) } }); - - await expect(helper.getHeartbeatJob()(makeMonitor())).rejects.toThrow("boom"); - expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "boom" })); - }); - - it("rethrows non-Error failures", async () => { - const { helper } = createHelper({ checkPipeline: { run: jest.fn().mockRejectedValue("unexpected") } }); - - await expect(helper.getHeartbeatJob()(makeMonitor())).rejects.toBe("unexpected"); - }); - }); - - // ── getHeartbeatGeoJob (glue) ───────────────────────────────────────────── - - describe("getHeartbeatGeoJob", () => { - it("runs the geo pipeline", async () => { - const { helper, defaults } = createHelper(); - - await helper.getHeartbeatGeoJob()(makeMonitor()); - - expect(defaults.geoCheckPipeline.run).toHaveBeenCalled(); - }); - - it("catches and logs a pipeline error without rethrowing", async () => { - const { helper, defaults } = createHelper({ geoCheckPipeline: { run: jest.fn().mockRejectedValue(new Error("api timeout")) } }); - - await expect(helper.getHeartbeatGeoJob()(makeMonitor())).resolves.toBeUndefined(); - expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "api timeout" })); - }); - - it("logs 'Unknown error' for non-Error failures", async () => { - const { helper, defaults } = createHelper({ geoCheckPipeline: { run: jest.fn().mockRejectedValue("string error") } }); - - await helper.getHeartbeatGeoJob()(makeMonitor()); - - expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined })); - }); - }); - // ── getCleanupOrphanedJob ──────────────────────────────────────────────── describe("getCleanupOrphanedJob", () => {