diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index 30db653e6..a7b127cfc 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -13,7 +13,6 @@ const SERVICE_NAME = "checkService"; export interface ICheckService { createChecks(checks: Check[]): Promise; toCheck(statusResponse: MonitorStatusResponse): Check | undefined; - toStatusResponse(check: Check): MonitorStatusResponse; getChecksByMonitor(params: { monitorId: string; teamId: string; @@ -135,32 +134,6 @@ export class CheckService implements ICheckService { return check; }; - private toPayload = (check: Check): HardwareStatusPayload | DockerStatusPayload | undefined => { - switch (check.metadata.type) { - case "hardware": - return { data: { cpu: check.cpu, memory: check.memory, disk: check.disk, host: check.host, net: check.net } } as HardwareStatusPayload; - case "docker": - return check.containers ? { containers: check.containers, summary: check.containerSummary } : undefined; - default: - return undefined; - } - }; - - toStatusResponse = (check: Check) => { - const statusResponse: MonitorStatusResponse = { - monitorId: check.metadata.monitorId, - teamId: check.metadata.teamId, - type: check.metadata.type, - status: check.status, - code: check.statusCode, - message: check.message, - responseTime: check.responseTime, - // re-nest hardware metrics so updateMonitorStatus / extractThresholdBreaches see payload.data - payload: this.toPayload(check), - }; - return statusResponse; - }; - getChecksByMonitor = async ({ monitorId, teamId, diff --git a/server/src/domain/checks/check.snapshot.ts b/server/src/domain/checks/check.snapshot.ts index 65ef4f60a..19bdbe8fa 100644 --- a/server/src/domain/checks/check.snapshot.ts +++ b/server/src/domain/checks/check.snapshot.ts @@ -45,12 +45,14 @@ const mapMemory = (memory?: CheckMemoryInfo): SnapshotMemoryInfo | undefined => }; const mapDisk = (disk?: CheckDiskInfo[]): SnapshotDiskInfo[] | undefined => - disk?.map((entry) => ({ - device: entry.device, - total_bytes: entry.total_bytes, - used_bytes: entry.used_bytes, - usage_percent: entry.usage_percent, - })); + disk + ?.filter((entry) => entry != null) + .map((entry) => ({ + device: entry.device, + total_bytes: entry.total_bytes, + used_bytes: entry.used_bytes, + usage_percent: entry.usage_percent, + })); const mapHost = (host?: CheckHostInfo): SnapshotHostInfo | undefined => host && { diff --git a/server/src/domain/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 1c249626a..e77fa5498 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -1,6 +1,6 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/domain/monitors/monitor.type.js"; -import type { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js"; +import type { Check } from "@/domain/checks/check.type.js"; import { AppError } from "@/utils/AppError.js"; import { getDateForRange } from "@/utils/dataUtils.js"; import type { IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js"; @@ -15,12 +15,7 @@ import { DateRange } from "@/types/query.js"; import { describeContainerBreach, findContainerBreaches } from "@/domain/docker/docker-alert.js"; export interface IIncidentService { - handleIncident( - monitor: Monitor, - code: number, - decision: MonitorActionDecision, - monitorStatusResponse?: MonitorStatusResponse - ): Promise; + handleIncident(monitor: Monitor, code: number, decision: MonitorActionDecision, check: Check): Promise; resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise; getIncidentsByTeam( teamId: string, @@ -59,12 +54,7 @@ export class IncidentService implements IIncidentService { this.notificationMessageBuilder = notificationMessageBuilder; } - handleIncident = async ( - monitor: Monitor, - code: number, - decision: MonitorActionDecision, - monitorStatusResponse?: MonitorStatusResponse - ): Promise => { + handleIncident = async (monitor: Monitor, code: number, decision: MonitorActionDecision, check: Check): Promise => { if (!decision.shouldCreateIncident && !decision.shouldResolveIncident) { return null; } @@ -81,7 +71,7 @@ export class IncidentService implements IIncidentService { // For threshold breaches, use 9999 status code and build descriptive message if (decision.incidentReason === "threshold_breach") { statusCode = 9999; - message = this.buildThresholdBreachMessage(monitor, monitorStatusResponse); + message = this.buildThresholdBreachMessage(monitor, check); } const incident = { @@ -106,18 +96,14 @@ export class IncidentService implements IIncidentService { return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); }; - private buildThresholdBreachMessage(monitor: Monitor, monitorStatusResponse?: MonitorStatusResponse): string { + private buildThresholdBreachMessage(monitor: Monitor, check: Check): string { if (monitor.type === "docker") { - const containers = (monitorStatusResponse?.payload as DockerStatusPayload | undefined)?.containers ?? []; + const containers = check.containers ?? []; const breaches = findContainerBreaches(monitor, containers); return breaches.length > 0 ? breaches.map(describeContainerBreach).join(", ") : "Container alert"; } - if (!monitorStatusResponse) { - return "Threshold breach detected"; - } - - const breaches = this.notificationMessageBuilder.extractThresholdBreaches(monitor, monitorStatusResponse); + const breaches = this.notificationMessageBuilder.extractThresholdBreaches(monitor, check); if (breaches.length === 0) { return "Threshold breach detected"; diff --git a/server/src/domain/notifications/notification.message-builder.ts b/server/src/domain/notifications/notification.message-builder.ts index 5cd78e9d9..11d11c069 100644 --- a/server/src/domain/notifications/notification.message-builder.ts +++ b/server/src/domain/notifications/notification.message-builder.ts @@ -1,5 +1,4 @@ import type { Monitor } from "@/domain/monitors/monitor.type.js"; -import type { DockerStatusPayload, HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import type { MonitorActionDecision } from "@/worker/worker.interface.js"; import type { NotificationMessage, @@ -9,15 +8,11 @@ import type { NotificationContent, } from "@/domain/notifications/notification.type.js"; import { describeContainerBreach, findContainerBreaches } from "@/domain/docker/docker-alert.js"; +import type { Check } from "@/domain/checks/check.type.js"; export interface INotificationMessageBuilder { - buildMessage( - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision, - clientHost: string - ): NotificationMessage; - extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[]; + buildMessage(monitor: Monitor, check: Check, decision: MonitorActionDecision, clientHost: string): NotificationMessage; + extractThresholdBreaches(monitor: Monitor, check: Check): ThresholdBreach[]; } const SERVICE_NAME = "NotificationMessageBuilder"; @@ -25,15 +20,10 @@ const SERVICE_NAME = "NotificationMessageBuilder"; export class NotificationMessageBuilder implements INotificationMessageBuilder { static SERVICE_NAME = SERVICE_NAME; - buildMessage( - monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision, - clientHost: string - ): NotificationMessage { + buildMessage(monitor: Monitor, check: Check, decision: MonitorActionDecision, clientHost: string): NotificationMessage { const type = this.determineNotificationType(decision, monitor); const severity = this.determineSeverity(type); - const content = this.buildContent(type, monitor, monitorStatusResponse); + const content = this.buildContent(type, monitor, check); return { type, @@ -92,18 +82,18 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } } - private buildContent(type: NotificationType, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { + private buildContent(type: NotificationType, monitor: Monitor, check: Check): NotificationContent { switch (type) { case "monitor_down": - return this.buildMonitorDownContent(monitor, monitorStatusResponse); + return this.buildMonitorDownContent(monitor, check); case "monitor_up": return this.buildMonitorUpContent(monitor); case "threshold_breach": - return this.buildThresholdBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse); + return this.buildThresholdBreachContent(monitor, check); case "threshold_resolved": return this.buildThresholdResolvedContent(monitor); case "container_breach": - return this.buildContainerBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse); + return this.buildContainerBreachContent(monitor, check); case "container_resolved": return this.buildContainersRecoveredContent(monitor); default: @@ -111,19 +101,19 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } } - private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { + private buildMonitorDownContent(monitor: Monitor, check: Check): NotificationContent { const title = `Monitor Down: ${monitor.name}`; const summary = `Monitor "${monitor.name}" is currently down and unreachable.`; const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`]; // Add response code if available - if (monitorStatusResponse.code) { - details.push(`Response Code: ${monitorStatusResponse.code}`); + if (check.statusCode) { + details.push(`Response Code: ${check.statusCode}`); } // Add error message if available - if (monitorStatusResponse.message) { - details.push(`Error: ${monitorStatusResponse.message}`); + if (check.message) { + details.push(`Error: ${check.message}`); } return { @@ -147,12 +137,12 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { }; } - private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { + private buildThresholdBreachContent(monitor: Monitor, check: Check): NotificationContent { const title = `Threshold Exceeded: ${monitor.name}`; const summary = `Monitor "${monitor.name}" has exceeded one or more thresholds.`; const details = [`URL: ${monitor.url}`, `Status: Threshold exceeded`, `Type: ${monitor.type}`]; - const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse); + const thresholds = this.extractThresholdBreaches(monitor, check); return { title, @@ -176,9 +166,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { }; } - private buildContainerBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { - const payload = monitorStatusResponse.payload; - const containers = payload && typeof payload !== "string" ? payload.containers : []; + private buildContainerBreachContent(monitor: Monitor, check: Check): NotificationContent { + const containers = check.containers ?? []; const breaches = findContainerBreaches(monitor, containers); const title = `Container Alert: ${monitor.name}`; const summary = `${breaches.length} container(s) on "${monitor.name}" need attention.`; @@ -202,25 +191,17 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { }; } - public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] { + public extractThresholdBreaches(monitor: Monitor, check: Check): ThresholdBreach[] { const breaches: ThresholdBreach[] = []; // Check if this is a hardware monitor with threshold data - if (monitor.type !== "hardware" || !monitorStatusResponse.payload || typeof monitorStatusResponse.payload === "string") { - return breaches; - } - - // Cast to HardwareStatusPayload type - const payload = monitorStatusResponse.payload; - const hardware = payload.data; - - if (!hardware) { + if (monitor.type !== "hardware") { return breaches; } // Note: usage_percent values in hardware payload are decimals (0-1) - if (monitor.cpuAlertThreshold !== undefined && monitor.cpuAlertThreshold !== null && hardware.cpu?.usage_percent !== undefined) { - const cpuUsageDecimal = hardware.cpu.usage_percent; + if (monitor.cpuAlertThreshold !== undefined && monitor.cpuAlertThreshold !== null && check.cpu?.usage_percent !== undefined) { + const cpuUsageDecimal = check.cpu.usage_percent; const cpuPercent = cpuUsageDecimal * 100; const threshold = monitor.cpuAlertThreshold; if (cpuPercent > threshold) { @@ -235,8 +216,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } // Memory threshold breach - if (monitor.memoryAlertThreshold !== undefined && monitor.memoryAlertThreshold !== null && hardware.memory?.usage_percent !== undefined) { - const memoryUsageDecimal = hardware.memory.usage_percent; + if (monitor.memoryAlertThreshold !== undefined && monitor.memoryAlertThreshold !== null && check.memory?.usage_percent !== undefined) { + const memoryUsageDecimal = check.memory.usage_percent; const memoryPercent = memoryUsageDecimal * 100; const threshold = monitor.memoryAlertThreshold; if (memoryPercent > threshold) { @@ -251,10 +232,10 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } // Disk threshold breach - if (monitor.diskAlertThreshold !== undefined && monitor.diskAlertThreshold !== null && Array.isArray(hardware.disk)) { + if (monitor.diskAlertThreshold !== undefined && monitor.diskAlertThreshold !== null && Array.isArray(check.disk)) { // Find the highest disk usage let maxDiskUsageDecimal = 0; - for (const disk of hardware.disk) { + for (const disk of check.disk) { if (disk.usage_percent !== undefined && disk.usage_percent > maxDiskUsageDecimal) { maxDiskUsageDecimal = disk.usage_percent; } @@ -273,9 +254,9 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } // Temperature threshold breach - if (monitor.tempAlertThreshold !== undefined && monitor.tempAlertThreshold !== null && hardware.cpu?.temperature) { + if (monitor.tempAlertThreshold !== undefined && monitor.tempAlertThreshold !== null && check.cpu?.temperature) { // Temperature is an array in cpu.temperature - const temps = Array.isArray(hardware.cpu.temperature) ? hardware.cpu.temperature : [hardware.cpu.temperature]; + const temps = Array.isArray(check.cpu.temperature) ? check.cpu.temperature : [check.cpu.temperature]; const maxTemp = Math.max(...temps.filter((t: number) => !isNaN(t))); const threshold = monitor.tempAlertThreshold; if (maxTemp > threshold) { diff --git a/server/src/domain/notifications/notification.service.ts b/server/src/domain/notifications/notification.service.ts index b852e8a49..b782b7e0a 100644 --- a/server/src/domain/notifications/notification.service.ts +++ b/server/src/domain/notifications/notification.service.ts @@ -1,6 +1,5 @@ import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Notification } from "@/domain/notifications/notification.type.js"; -import type { MonitorStatusResponse } from "@/types/network.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import { INotificationsRepository } from "@/domain/notifications/notification.repository.interface.js"; @@ -10,6 +9,7 @@ import type { ISettingsService } from "@/domain/app-settings/app-settings.servic import { ILogger } from "@/utils/logger.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; import type { NotificationChannel } from "@/domain/notifications/notification.type.js"; +import type { Check } from "@/domain/checks/check.type.js"; export type NotificationProviderRegistry = Record; @@ -19,7 +19,7 @@ export interface INotificationsService { findNotificationsByTeamId: (teamId: string) => Promise; updateById(id: string, teamId: string, updateData: Partial): Promise; deleteById: (id: string, teamId: string) => Promise; - handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise; + handleNotifications: (monitor: Monitor, check: Check, decision: MonitorActionDecision) => Promise; sendTestNotification: (notification: Partial) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; @@ -63,7 +63,7 @@ export class NotificationsService implements INotificationsService { private send = async ( notification: Notification, monitor: Monitor, - monitorStatusResponse: MonitorStatusResponse, + check: Check, decision: MonitorActionDecision, notificationMessage: NotificationMessage | undefined ): Promise => { @@ -89,16 +89,16 @@ export class NotificationsService implements INotificationsService { return await provider.sendMessage(notification, notificationMessage); }; - private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + private sendNotifications = async (monitor: Monitor, check: Check, decision: MonitorActionDecision) => { const notificationIds = monitor.notifications ?? []; const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); // Build notification message once for all notifications const settings = this.settingsService.getSettings(); const clientHost = settings.clientHost || "Host not defined"; - const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost); + const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, check, decision, clientHost); - const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision, notificationMessage)); + const tasks = notifications.map((notification) => this.send(notification, monitor, check, decision, notificationMessage)); const outcomes = await Promise.all(tasks); const succeeded = outcomes.filter(Boolean).length; @@ -114,13 +114,13 @@ export class NotificationsService implements INotificationsService { return succeeded === notifications.length; }; - handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { + handleNotifications = async (monitor: Monitor, check: Check, decision: MonitorActionDecision) => { if (!decision.shouldSendNotification) { return false; } // Send notifications based on decision - return await this.sendNotifications(monitor, monitorStatusResponse, decision); + return await this.sendNotifications(monitor, check, decision); }; sendTestNotification = async (notification: Partial) => { diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts index 13ae68109..64a6f2e61 100755 --- a/server/src/service/statusService.ts +++ b/server/src/service/statusService.ts @@ -2,18 +2,7 @@ import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.re import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import type { Check, CheckDiskInfo } from "@/domain/checks/check.type.js"; import { MonitorStatuses, type Monitor, type MonitorStatus } from "@/domain/monitors/monitor.type.js"; -import type { - DockerStatusPayload, - GameStatusPayload, - GrpcStatusPayload, - HardwareStatusPayload, - HttpStatusPayload, - MonitorStatusResponse, - PageSpeedStatusPayload, - PingStatusPayload, - PortStatusPayload, - StatusChangeResult, -} from "@/types/network.js"; +import type { StatusChangeResult } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ILogger } from "@/utils/logger.js"; import type { HardwareStatusMetrics } from "@/types/network.js"; @@ -29,22 +18,8 @@ type HardwareBreaches = Record; type HardwareCounters = Record; export interface IStatusService { - updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse): Promise; - updateMonitorStatus( - statusResponse: MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - | undefined - >, - check: Check, - monitor: Monitor - ): Promise; + updateRunningStats(check: Check, monitor: Monitor): Promise; + updateMonitorStatus(check: Check, monitor: Monitor): Promise; } export class StatusService implements IStatusService { @@ -59,11 +34,11 @@ export class StatusService implements IStatusService { this.monitorStatsRepository = monitorStatsRepository; } - async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) { + async updateRunningStats(check: Check, monitor: Monitor) { try { await this.monitorStatsRepository.updateByMonitorId(monitor.id, { - status: networkResponse.status === true, - responseTime: networkResponse.responseTime ?? 0, + status: check.status === true, + responseTime: check.responseTime ?? 0, now: Date.now(), }); return true; @@ -78,8 +53,8 @@ export class StatusService implements IStatusService { } } - private tryUpdateRunningStats = async (monitor: Monitor, statusResponse: MonitorStatusResponse) => { - const statsOk = await this.updateRunningStats(monitor, statusResponse); + private tryUpdateRunningStats = async (check: Check, monitor: Monitor) => { + const statsOk = await this.updateRunningStats(check, monitor); if (!statsOk) { this.logger.warn({ service: SERVICE_NAME, @@ -180,26 +155,12 @@ export class StatusService implements IStatusService { return { nextStatus, transitioned }; }; - updateMonitorStatus = async ( - statusResponse: MonitorStatusResponse< - | PingStatusPayload - | HttpStatusPayload - | PageSpeedStatusPayload - | HardwareStatusPayload - | DockerStatusPayload - | PortStatusPayload - | GameStatusPayload - | GrpcStatusPayload - | undefined - >, - check: Check, - monitor: Monitor - ): Promise => { + updateMonitorStatus = async (check: Check, monitor: Monitor): Promise => { try { - const { status, code } = statusResponse; + const { status, statusCode } = check; // Update running stats - await this.tryUpdateRunningStats(monitor, statusResponse); + await this.tryUpdateRunningStats(check, monitor); const prevStatus = monitor.status; const checkSnapshot = toCheckSnapshot(check); @@ -240,7 +201,7 @@ export class StatusService implements IStatusService { monitor: updated, statusChanged, prevStatus, - code, + code: statusCode, timestamp: Date.now(), }; } @@ -253,14 +214,13 @@ export class StatusService implements IStatusService { statusChanged = true; } - // Evaluate hardware threshold breaches (only for hardware monitors with metrics payload) + // Evaluate hardware threshold breaches let thresholdBreaches: HardwareBreaches | undefined; - const hardwarePayload = statusResponse.payload as HardwareStatusPayload | undefined; - if (monitor.type === "hardware" && hardwarePayload?.data) { + if (monitor.type === "hardware") { const hardware = this.computeHardwareStatus({ currentStatus: newStatus, reachabilityDown: newStatus === "down", - metrics: hardwarePayload.data, + metrics: check, thresholds: { cpu: monitor.cpuAlertThreshold, memory: monitor.memoryAlertThreshold, @@ -286,9 +246,8 @@ export class StatusService implements IStatusService { } } - const dockerPayload = statusResponse.payload as DockerStatusPayload | undefined; - if (monitor.type === "docker" && dockerPayload?.containers) { - const breaches = findContainerBreaches(monitor, dockerPayload.containers); + if (monitor.type === "docker" && check.containers) { + const breaches = findContainerBreaches(monitor, check.containers); const docker = this.computeDockerStatus({ currentStatus: newStatus, reachabilityDown: newStatus === "down", @@ -317,7 +276,7 @@ export class StatusService implements IStatusService { monitor: updated, statusChanged, prevStatus, - code, + code: statusCode, timestamp: new Date().getTime(), thresholdBreaches, }; diff --git a/server/src/worker/reactors/reactor.incident.ts b/server/src/worker/reactors/reactor.incident.ts index 3679a30cf..24ca60e7b 100644 --- a/server/src/worker/reactors/reactor.incident.ts +++ b/server/src/worker/reactors/reactor.incident.ts @@ -8,6 +8,6 @@ export class IncidentReactor implements IMonitorReactor { constructor(private incidentService: IIncidentService) {} react = async (evaluation: MonitorEvaluation) => { - await this.incidentService.handleIncident(evaluation.monitor, evaluation.statusChange.code, evaluation.decision, evaluation.status); + await this.incidentService.handleIncident(evaluation.monitor, evaluation.statusChange.code, evaluation.decision, evaluation.check); }; } diff --git a/server/src/worker/reactors/reactor.notification.ts b/server/src/worker/reactors/reactor.notification.ts index 1daf528f4..91b9b7cd4 100644 --- a/server/src/worker/reactors/reactor.notification.ts +++ b/server/src/worker/reactors/reactor.notification.ts @@ -8,6 +8,6 @@ export class NotificationReactor implements IMonitorReactor { react = async (evaluation: MonitorEvaluation) => { if (!evaluation.decision.shouldSendNotification) return; - await this.notificationService.handleNotifications(evaluation.monitor, evaluation.status, evaluation.decision); + await this.notificationService.handleNotifications(evaluation.monitor, evaluation.check, evaluation.decision); }; } diff --git a/server/src/worker/worker.interface.ts b/server/src/worker/worker.interface.ts index d29b95fbb..4b5bba2c5 100644 --- a/server/src/worker/worker.interface.ts +++ b/server/src/worker/worker.interface.ts @@ -1,7 +1,7 @@ import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Check } from "@/domain/checks/check.type.js"; import { Job, JobType } from "@/domain/jobs/job.type.js"; -import { MonitorPayloadMap, MonitorStatusResponse, StatusChangeResult } from "@/types/network.js"; +import { StatusChangeResult } from "@/types/network.js"; import { QueueWorker } from "@/domain/queue-workers/queue-worker.type.js"; import type { QueueMode } from "@/domain/app-settings/app-settings.type.js"; @@ -23,7 +23,6 @@ export interface MonitorActionDecision { export type MonitorEvaluation = { monitor: Monitor; - status: MonitorStatusResponse; // raw result from networkService.requestStatus check: Check; statusChange: StatusChangeResult; // from statusService.updateMonitorStatus decision: MonitorActionDecision; diff --git a/server/src/worker/worker.pipeline.ts b/server/src/worker/worker.pipeline.ts index 658737826..1faef2a11 100644 --- a/server/src/worker/worker.pipeline.ts +++ b/server/src/worker/worker.pipeline.ts @@ -240,8 +240,7 @@ export class WorkerPipeline implements IWorkerPipeline { let current = monitor; for (const check of checks) { - const status = this.deps.checkService.toStatusResponse(check); - const evaluation = await this.evaluateCheck(status, check, current); + const evaluation = await this.evaluateCheck(check, current); await this.deps.dispatcher.dispatch(evaluation); // Handle incidents and notifications const leaseHeld = await this.deps.jobsRepository.pullEvaluated(job.id, [check.id]); if (!leaseHeld) { @@ -272,7 +271,7 @@ export class WorkerPipeline implements IWorkerPipeline { // A check taken while the instance had no outbound connectivity says nothing about the target. // It is left out of the status window, running stats and monitor status entirely, so that no incident // opens for it and no spurious "resolved"/"up" fires once egress returns. - private skipDegradedEgressCheck = (status: MonitorStatusResponse, check: Check, monitor: Monitor): MonitorEvaluation => { + private skipDegradedEgressCheck = (check: Check, monitor: Monitor): MonitorEvaluation => { this.deps.logger.debug({ message: `Skipping evaluation of check ${check.id} for monitor ${monitor.id}: instance egress was degraded`, service: SERVICE_NAME, @@ -287,22 +286,21 @@ export class WorkerPipeline implements IWorkerPipeline { }; return { monitor, - status, check, - statusChange: { monitor, statusChanged: false, prevStatus: monitor.status, code: status.code, timestamp: Date.now() }, + statusChange: { monitor, statusChanged: false, prevStatus: monitor.status, code: check.statusCode, timestamp: Date.now() }, decision, }; }; - evaluateCheck = async (status: MonitorStatusResponse, check: Check, monitor: Monitor): Promise => { + evaluateCheck = async (check: Check, monitor: Monitor): Promise => { if (check.egressStatus === "degraded") { - return this.skipDegradedEgressCheck(status, check, monitor); + return this.skipDegradedEgressCheck(check, monitor); } - const statusChange = await this.deps.statusService.updateMonitorStatus(status, check, monitor); + const statusChange = await this.deps.statusService.updateMonitorStatus(check, monitor); const decision = this.decide(statusChange); - return { monitor: statusChange.monitor, status, check, statusChange, decision }; + return { monitor: statusChange.monitor, check, statusChange, decision }; }; private decide = (statusChange: StatusChangeResult): MonitorActionDecision => { diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index c4de7f6ec..f99be611c 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -5,17 +5,15 @@ import { IncidentReactor } from "../../src/worker/reactors/reactor.incident.ts"; import { ReactorDispatcher } from "../../src/worker/reactors/reactor.dispatcher.ts"; import { StatusService } from "../../src/service/statusService.ts"; import { IncidentService } from "../../src/domain/incidents/incident.service.ts"; +import { CheckService } from "../../src/domain/checks/check.service.ts"; import { InMemoryMonitorsRepository } from "./InMemoryMonitorsRepository.ts"; import { InMemoryIncidentsRepository } from "./InMemoryIncidentsRepository.ts"; import { createMockLogger } from "./createMockLogger.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { MonitorStatusResponse } from "../../src/types/network.ts"; -import type { Check } from "../../src/domain/checks/check.type.ts"; import type { MaintenanceWindow } from "../../src/domain/maintenance-windows/maintenance-window.type.ts"; import type { EgressStatus } from "../../src/domain/egress/egress.type.ts"; -let checkCounter = 0; - export const makeMonitor = (overrides?: Partial): Monitor => ({ id: "mon-1", @@ -45,20 +43,6 @@ export const makeStatusResponse = (status: boolean, code: number): MonitorStatus responseTime: status ? 150 : 0, }); -export const makeCheck = (status: boolean, code: number): Check => { - const now = new Date().toISOString(); - return { - id: `check-${++checkCounter}`, - metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, - status, - statusCode: code, - responseTime: status ? 150 : 0, - message: status ? "OK" : "Service Unavailable", - createdAt: now, - updatedAt: now, - }; -}; - const createStubMonitorStatsRepo = () => ({ findByMonitorId: jest.fn().mockRejectedValue(new Error("no stats")), create: jest.fn().mockResolvedValue({}), @@ -86,8 +70,6 @@ export interface HeartbeatTestHarness { } export function createHeartbeatTestHarness(): HeartbeatTestHarness { - checkCounter = 0; - const monitorsRepo = new InMemoryMonitorsRepository(); const incidentsRepo = new InMemoryIncidentsRepository(); const logger = createMockLogger() as any; @@ -111,11 +93,9 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { return Promise.resolve(makeStatusResponse(nextStatus, nextCode)); }), }; - const checkService = { - toCheck: jest.fn().mockImplementation((response: MonitorStatusResponse) => { - return makeCheck(response.status, response.code); - }), - }; + // The real mapper, so the stored check carries the hardware/docker payload the evaluator reads. + // Only toCheck is reached; the repositories are never touched. + const checkService = new CheckService(monitorsRepo as any, logger, {} as any); const setNextResponse = (status: boolean, code: number) => { nextResponse = null; @@ -165,7 +145,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { if (!monitor) return; const result = await pipeline.produce(monitor); if (!result) return; // skipped (e.g. maintenance window) - const evaluation = await pipeline.evaluateCheck(result.status, result.check, monitor); + const evaluation = await pipeline.evaluateCheck(result.check, monitor); await reactorDispatcher.dispatch(evaluation); }; diff --git a/server/test/integration/evaluateCursorMultiWorker.test.ts b/server/test/integration/evaluateCursorMultiWorker.test.ts index 50be5ea72..a3fe75b04 100644 --- a/server/test/integration/evaluateCursorMultiWorker.test.ts +++ b/server/test/integration/evaluateCursorMultiWorker.test.ts @@ -131,7 +131,7 @@ describe("Evaluate cursor with more than one processing worker", () => { bufferService, dockerLogsService: { buildDockerLogs: jest.fn() } as any, statusService: { - updateMonitorStatus: async (_status: MonitorStatusResponse, check: Check, monitor: Monitor) => { + updateMonitorStatus: async (check: Check, monitor: Monitor) => { evaluatedCheckIds.push(check.id); return { monitor, statusChanged: false, prevStatus: monitor.status, code: 200, timestamp: 0 }; }, diff --git a/server/test/unit/services/checkService.test.ts b/server/test/unit/services/checkService.test.ts index 230857b0e..daf238036 100644 --- a/server/test/unit/services/checkService.test.ts +++ b/server/test/unit/services/checkService.test.ts @@ -3,9 +3,7 @@ import { CheckService } from "../../../src/domain/checks/check.service.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; -import type { MonitorStatusResponse, HardwareStatusPayload, PageSpeedStatusPayload, DockerStatusPayload } from "../../../src/types/network.ts"; -import { NotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; +import type { MonitorStatusResponse, HardwareStatusPayload, PageSpeedStatusPayload } from "../../../src/types/network.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -281,112 +279,6 @@ describe("CheckService", () => { }); }); - // ── toStatusResponse round-trip (Phase 2 §5 guard) ───────────────────────── - // The DB-queue split persists a Check, then the evaluator rebuilds the status response from it. - // toStatusResponse is only correct because every field a downstream consumer reads survives - // toCheck → toStatusResponse. These tests pin that: if a future change drops a consumed field - // from the round-trip, they fail instead of silently regressing the evaluator/reactors. - - describe("toStatusResponse round-trip", () => { - const makeHardwareStatus = (overrides?: Partial): MonitorStatusResponse => - makeStatusResponse({ - type: "hardware", - status: true, - code: 200, - message: "OK", - responseTime: 42, - payload: { - data: { - cpu: { usage_percent: 0.92 }, - memory: { usage_percent: 0.81 }, - disk: [{ device: "/dev/sda", usage_percent: 0.75 }], - host: { os: "linux" }, - net: [{ name: "eth0" }], - }, - } as HardwareStatusPayload, - ...overrides, - } as any); - - it("uptime: preserves every consumed scalar field", () => { - const { service } = createService(); - const status = makeStatusResponse({ type: "http", status: false, code: 503, message: "Service Unavailable", responseTime: 87 }); - - const rebuilt = service.toStatusResponse(service.toCheck(status)!); - - expect(rebuilt.monitorId).toBe(status.monitorId); - expect(rebuilt.teamId).toBe(status.teamId); - expect(rebuilt.type).toBe(status.type); - expect(rebuilt.status).toBe(status.status); - expect(rebuilt.code).toBe(status.code); - expect(rebuilt.message).toBe(status.message); - expect(rebuilt.responseTime).toBe(status.responseTime); - }); - - it("hardware: preserves payload.data metrics verbatim, same units (usage_percent stays a 0–1 decimal)", () => { - const { service } = createService(); - const status = makeHardwareStatus(); - - const rebuilt = service.toStatusResponse(service.toCheck(status)!); - - const original = status.payload as HardwareStatusPayload; - const roundtripped = rebuilt.payload as HardwareStatusPayload; - expect(roundtripped.data).toEqual(original.data); - expect(roundtripped.data.cpu!.usage_percent).toBe(0.92); // not rescaled to 92 - }); - - it("hardware: a real consumer (extractThresholdBreaches) behaves identically on raw vs round-tripped status", () => { - const { service } = createService(); - const builder = new NotificationMessageBuilder(); - const monitor = { - id: "mon-1", - type: "hardware", - cpuAlertThreshold: 90, // 0.92 → 92% breaches - memoryAlertThreshold: 90, // 0.81 → 81% does not - diskAlertThreshold: 90, // 0.75 → 75% does not - tempAlertThreshold: 100, - } as Monitor; - const status = makeHardwareStatus(); - - const rebuilt = service.toStatusResponse(service.toCheck(status)!); - - const fromRaw = builder.extractThresholdBreaches(monitor, status as MonitorStatusResponse); - const fromRebuilt = builder.extractThresholdBreaches(monitor, rebuilt as MonitorStatusResponse); - - // sanity: the fixture actually triggers a breach, so the equality below is not vacuous - expect(fromRaw).toHaveLength(1); - expect(fromRaw[0].metric).toBe("cpu"); - expect(fromRebuilt).toEqual(fromRaw); - }); - - it("docker: preserves payload.containers and payload.summary verbatim", () => { - const { service } = createService(); - const payload: DockerStatusPayload = { - containers: [ - { id: "1", name: "db", image: "postgres:16", state: "exited", status: "Exited (1) 2 minutes ago", health: "none", exitCode: 1 }, - { id: "2", name: "api", image: "api:latest", state: "running", status: "Up 3 hours (unhealthy)", health: "unhealthy", restartCount: 2 }, - ], - summary: { total: 2, running: 1, stopped: 1, unhealthy: 1 }, - }; - const status = makeStatusResponse({ type: "docker", status: true, payload } as any); - - const rebuilt = service.toStatusResponse(service.toCheck(status)!); - - const roundtripped = rebuilt.payload as DockerStatusPayload; - expect(roundtripped.containers).toEqual(payload.containers); - expect(roundtripped.summary).toEqual(payload.summary); - expect(roundtripped.containers[0].exitCode).toBe(1); - }); - - it("docker: leaves payload undefined for a failed check with no containers", () => { - const { service } = createService(); - const status = makeStatusResponse({ type: "docker", status: false, code: 5000, message: "connect ECONNREFUSED", payload: null } as any); - - const rebuilt = service.toStatusResponse(service.toCheck(status)!); - - expect(rebuilt.payload).toBeUndefined(); - }); - }); - // ── createChecks ───────────────────────────────────────────────────────── describe("createChecks", () => { diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index a2e161769..f5f978327 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.ts @@ -8,6 +8,7 @@ import type { INotificationMessageBuilder } from "../../../src/domain/notificati import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { Incident } from "../../../src/domain/incidents/incident.type.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.interface.ts"; +import type { Check } from "../../../src/domain/checks/check.type.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -79,6 +80,19 @@ const makeIncident = (overrides?: Partial): Incident => ...overrides, }) as Incident; +const makeCheck = (overrides?: Partial): Check => + ({ + id: "check-1", + metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, + status: true, + statusCode: 200, + responseTime: 100, + message: "OK", + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T00:00:00.000Z", + ...overrides, + }) as Check; + const makeDecision = (overrides?: Partial): MonitorActionDecision => ({ shouldCreateIncident: false, shouldResolveIncident: false, @@ -96,7 +110,7 @@ describe("IncidentService", () => { describe("handleIncident", () => { it("returns null when neither create nor resolve is requested", async () => { const { service } = createService(); - const result = await service.handleIncident(makeMonitor(), 200, makeDecision()); + const result = await service.handleIncident(makeMonitor(), 200, makeDecision(), makeCheck()); expect(result).toBeNull(); }); @@ -105,7 +119,12 @@ describe("IncidentService", () => { const { service, incidentsRepository } = createService(); (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(existing); - const result = await service.handleIncident(makeMonitor(), 500, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + const result = await service.handleIncident( + makeMonitor(), + 500, + makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), + makeCheck() + ); expect(result).toBe(existing); expect(incidentsRepository.create).not.toHaveBeenCalled(); @@ -117,7 +136,12 @@ describe("IncidentService", () => { (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - const result = await service.handleIncident(makeMonitor(), 500, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + const result = await service.handleIncident( + makeMonitor(), + 500, + makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), + makeCheck() + ); expect(result).toBe(created); expect(incidentsRepository.create).toHaveBeenCalledWith( @@ -139,9 +163,12 @@ describe("IncidentService", () => { { metric: "cpu", formattedValue: "95%", threshold: 80, unit: "%" }, ]); + const monitor = makeMonitor({ type: "hardware" }); + const check = makeCheck({ metadata: { monitorId: "mon-1", teamId: "team-1", type: "hardware" }, cpu: { usage_percent: 0.95 } }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, { monitorId: "mon-1" } as any); + await service.handleIncident(monitor, 200, decision, check); + expect(notificationMessageBuilder.extractThresholdBreaches).toHaveBeenCalledWith(monitor, check); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 9999, @@ -150,18 +177,6 @@ describe("IncidentService", () => { ); }); - it("uses fallback message when monitorStatusResponse is undefined for threshold_breach", async () => { - const created = makeIncident(); - const { service, incidentsRepository } = createService(); - (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - - const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, undefined); - - expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ message: "Threshold breach detected" })); - }); - it("uses fallback message when extractThresholdBreaches returns empty array", async () => { const created = makeIncident(); const { service, incidentsRepository, notificationMessageBuilder } = createService(); @@ -170,7 +185,7 @@ describe("IncidentService", () => { (notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([]); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, { monitorId: "mon-1" } as any); + await service.handleIncident(makeMonitor(), 200, decision, makeCheck()); expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ message: "Threshold breach detected" })); }); @@ -182,19 +197,17 @@ describe("IncidentService", () => { (incidentsRepository.create as jest.Mock).mockResolvedValue(created); const monitor = makeMonitor({ type: "docker", status: "breached", dockerAlertOnStopped: true, dockerAlertOnUnhealthy: true }); - const response = { - monitorId: "mon-1", - payload: { - containers: [ - { id: "1", name: "db", image: "postgres", state: "exited", status: "Exited (1)", health: "none", exitCode: 1 }, - { id: "2", name: "api", image: "api", state: "running", status: "Up", health: "unhealthy" }, - { id: "3", name: "web", image: "nginx", state: "running", status: "Up", health: "healthy" }, - ], - summary: { total: 3, running: 2, stopped: 1, unhealthy: 1 }, - }, - } as any; + const check = makeCheck({ + metadata: { monitorId: "mon-1", teamId: "team-1", type: "docker" }, + containers: [ + { id: "1", name: "db", image: "postgres", state: "exited", status: "Exited (1)", health: "none", exitCode: 1 }, + { id: "2", name: "api", image: "api", state: "running", status: "Up", health: "unhealthy" }, + { id: "3", name: "web", image: "nginx", state: "running", status: "Up", health: "healthy" }, + ], + containerSummary: { total: 3, running: 2, stopped: 1, unhealthy: 1 }, + }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(monitor, 200, decision, response); + await service.handleIncident(monitor, 200, decision, check); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 9999, message: "db: stopped (exit code 1), api: unhealthy" }) @@ -202,19 +215,6 @@ describe("IncidentService", () => { expect(notificationMessageBuilder.extractThresholdBreaches).not.toHaveBeenCalled(); }); - it("uses the container fallback message for docker incidents with no response", async () => { - const created = makeIncident(); - const { service, incidentsRepository } = createService(); - (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - - const monitor = makeMonitor({ type: "docker", status: "breached", dockerAlertOnStopped: true }); - const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(monitor, 200, decision, undefined); - - expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ statusCode: 9999, message: "Container alert" })); - }); - it("joins multiple threshold breaches with commas", async () => { const created = makeIncident(); const { service, incidentsRepository, notificationMessageBuilder } = createService(); @@ -226,7 +226,7 @@ describe("IncidentService", () => { ]); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, { monitorId: "mon-1" } as any); + await service.handleIncident(makeMonitor(), 200, decision, makeCheck()); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ message: "CPU: 95% (threshold: 80%), MEMORY: 90% (threshold: 80%)" }) @@ -240,7 +240,7 @@ describe("IncidentService", () => { (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(active); (incidentsRepository.updateById as jest.Mock).mockResolvedValue(resolved); - const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true })); + const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true }), makeCheck()); expect(result).toBe(resolved); expect(incidentsRepository.updateById).toHaveBeenCalledWith( @@ -254,7 +254,7 @@ describe("IncidentService", () => { const { service, incidentsRepository } = createService(); (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true })); + const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true }), makeCheck()); expect(result).toBeNull(); }); diff --git a/server/test/unit/services/notificationMessageBuilder.test.ts b/server/test/unit/services/notificationMessageBuilder.test.ts index afa6519e9..3937e3bcf 100644 --- a/server/test/unit/services/notificationMessageBuilder.test.ts +++ b/server/test/unit/services/notificationMessageBuilder.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, beforeEach } from "@jest/globals"; import { NotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; -import type { MonitorStatusResponse, HardwareStatusPayload, DockerStatusPayload } from "../../../src/types/network.ts"; +import type { Check } from "../../../src/domain/checks/check.type.ts"; +import type { DockerContainerInfo } from "../../../src/domain/docker/docker.type.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.interface.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -30,29 +31,20 @@ const makeDecision = (overrides?: Partial): MonitorAction ...overrides, }); -const makeStatusResponse = (overrides?: Partial): MonitorStatusResponse => - ({ - monitorId: "mon-1", - teamId: "team-1", - type: "http", - status: false, - code: 500, - message: "Internal Server Error", - ...overrides, - }) as MonitorStatusResponse; +const makeCheck = (overrides?: Partial): Check => ({ + id: "check-1", + metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, + status: false, + responseTime: 100, + statusCode: 500, + message: "Internal Server Error", + createdAt: "2026-01-01T00:00:00Z", + updatedAt: "2026-01-01T00:00:00Z", + ...overrides, +}); -const makeHardwarePayload = (overrides?: Partial): HardwareStatusPayload => - ({ - data: { - cpu: { usage_percent: 0.5, temperature: [45] }, - memory: { usage_percent: 0.6 }, - disk: [{ usage_percent: 0.7 }], - ...overrides, - }, - }) as HardwareStatusPayload; - -const makeDockerPayload = (containers: Partial[]): DockerStatusPayload => ({ - containers: containers.map((container, index) => ({ +const makeContainers = (containers: Partial[]): DockerContainerInfo[] => + containers.map((container, index) => ({ id: `container-${index}`, name: `container-${index}`, image: "nginx:latest", @@ -60,9 +52,7 @@ const makeDockerPayload = (containers: Partial): Monitor => makeMonitor({ @@ -95,9 +85,9 @@ describe("NotificationMessageBuilder", () => { it("builds a monitor_down message", () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision(); - const response = makeStatusResponse(); + const check = makeCheck(); - const msg = builder.buildMessage(monitor, response, decision, "https://app.example.com"); + const msg = builder.buildMessage(monitor, check, decision, "https://app.example.com"); expect(msg.type).toBe("monitor_down"); expect(msg.severity).toBe("critical"); @@ -120,7 +110,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "http" }); const decision = makeDecision({ notificationReason: "status_change" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, "https://app.example.com"); + const msg = builder.buildMessage(monitor, makeCheck(), decision, "https://app.example.com"); expect(msg.type).toBe("monitor_up"); expect(msg.severity).toBe("success"); @@ -130,12 +120,9 @@ describe("NotificationMessageBuilder", () => { it("builds a threshold_breach message", () => { const monitor = makeMonitor({ status: "up", type: "hardware", cpuAlertThreshold: 80 }); const decision = makeDecision({ notificationReason: "threshold_breach" }); - const response = makeStatusResponse({ - type: "hardware", - payload: makeHardwarePayload({ cpu: { usage_percent: 0.9, temperature: [50] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.9, temperature: [50] } }); - const msg = builder.buildMessage(monitor, response, decision, "https://app.example.com"); + const msg = builder.buildMessage(monitor, check, decision, "https://app.example.com"); expect(msg.type).toBe("threshold_breach"); expect(msg.severity).toBe("warning"); @@ -148,7 +135,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "hardware" }); const decision = makeDecision({ notificationReason: "threshold_resolved" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, "https://app.example.com"); + const msg = builder.buildMessage(monitor, makeCheck(), decision, "https://app.example.com"); expect(msg.type).toBe("threshold_resolved"); expect(msg.severity).toBe("success"); @@ -159,7 +146,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ notificationReason: null }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, "https://app.example.com"); + const msg = builder.buildMessage(monitor, makeCheck(), decision, "https://app.example.com"); expect(msg.metadata.notificationReason).toBe("status_change"); }); @@ -172,7 +159,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ notificationReason: "threshold_breach" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("monitor_down"); }); @@ -181,7 +168,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "hardware" }); const decision = makeDecision({ notificationReason: "threshold_breach" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("threshold_breach"); }); @@ -190,7 +177,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "hardware" }); const decision = makeDecision({ notificationReason: "threshold_resolved" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("threshold_resolved"); }); @@ -199,7 +186,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "hardware" }); const decision = makeDecision({ notificationReason: "status_change" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("monitor_up"); }); @@ -207,7 +194,7 @@ describe("NotificationMessageBuilder", () => { it("returns container_breach for docker monitor with threshold_breach reason", () => { const decision = makeDecision({ notificationReason: "threshold_breach" }); - const msg = builder.buildMessage(makeDockerMonitor(), makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(makeDockerMonitor(), makeCheck(), decision, ""); expect(msg.type).toBe("container_breach"); }); @@ -215,7 +202,7 @@ describe("NotificationMessageBuilder", () => { it("returns container_resolved for docker monitor with threshold_resolved reason", () => { const decision = makeDecision({ notificationReason: "threshold_resolved" }); - const msg = builder.buildMessage(makeDockerMonitor({ status: "up" }), makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(makeDockerMonitor({ status: "up" }), makeCheck(), decision, ""); expect(msg.type).toBe("container_resolved"); }); @@ -223,7 +210,7 @@ describe("NotificationMessageBuilder", () => { it("returns monitor_down for docker monitor that is down regardless of reason", () => { const decision = makeDecision({ notificationReason: "threshold_breach" }); - const msg = builder.buildMessage(makeDockerMonitor({ status: "down" }), makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(makeDockerMonitor({ status: "down" }), makeCheck(), decision, ""); expect(msg.type).toBe("monitor_down"); }); @@ -232,7 +219,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "up", type: "http" }); const decision = makeDecision({ notificationReason: "status_change" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("monitor_up"); }); @@ -241,7 +228,7 @@ describe("NotificationMessageBuilder", () => { const monitor = makeMonitor({ status: "unknown" as any }); const decision = makeDecision({ notificationReason: null }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), decision, ""); + const msg = builder.buildMessage(monitor, makeCheck(), decision, ""); expect(msg.type).toBe("monitor_up"); }); @@ -251,14 +238,14 @@ describe("NotificationMessageBuilder", () => { describe("determineSeverity", () => { it("returns critical for monitor_down", () => { - const msg = builder.buildMessage(makeMonitor({ status: "down" }), makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(makeMonitor({ status: "down" }), makeCheck(), makeDecision(), ""); expect(msg.severity).toBe("critical"); }); it("returns warning for threshold_breach", () => { const msg = builder.buildMessage( makeMonitor({ status: "up", type: "hardware" }), - makeStatusResponse(), + makeCheck(), makeDecision({ notificationReason: "threshold_breach" }), "" ); @@ -268,7 +255,7 @@ describe("NotificationMessageBuilder", () => { it("returns success for monitor_up", () => { const msg = builder.buildMessage( makeMonitor({ status: "up", type: "http" }), - makeStatusResponse(), + makeCheck(), makeDecision({ notificationReason: "status_change" }), "" ); @@ -278,7 +265,7 @@ describe("NotificationMessageBuilder", () => { it("returns success for threshold_resolved", () => { const msg = builder.buildMessage( makeMonitor({ status: "up", type: "hardware" }), - makeStatusResponse(), + makeCheck(), makeDecision({ notificationReason: "threshold_resolved" }), "" ); @@ -286,14 +273,14 @@ describe("NotificationMessageBuilder", () => { }); it("returns warning for container_breach", () => { - const msg = builder.buildMessage(makeDockerMonitor(), makeStatusResponse(), makeDecision({ notificationReason: "threshold_breach" }), ""); + const msg = builder.buildMessage(makeDockerMonitor(), makeCheck(), makeDecision({ notificationReason: "threshold_breach" }), ""); expect(msg.severity).toBe("warning"); }); it("returns success for container_resolved", () => { const msg = builder.buildMessage( makeDockerMonitor({ status: "up" }), - makeStatusResponse(), + makeCheck(), makeDecision({ notificationReason: "threshold_resolved" }), "" ); @@ -307,9 +294,9 @@ describe("NotificationMessageBuilder", () => { describe("monitor_down", () => { it("includes response code and error message when present", () => { const monitor = makeMonitor({ status: "down" }); - const response = makeStatusResponse({ code: 503, message: "Service Unavailable" }); + const check = makeCheck({ statusCode: 503, message: "Service Unavailable" }); - const msg = builder.buildMessage(monitor, response, makeDecision(), ""); + const msg = builder.buildMessage(monitor, check, makeDecision(), ""); expect(msg.content.details).toContain("Response Code: 503"); expect(msg.content.details).toContain("Error: Service Unavailable"); @@ -317,9 +304,9 @@ describe("NotificationMessageBuilder", () => { it("omits response code when falsy", () => { const monitor = makeMonitor({ status: "down" }); - const response = makeStatusResponse({ code: 0, message: "" }); + const check = makeCheck({ statusCode: 0, message: "" }); - const msg = builder.buildMessage(monitor, response, makeDecision(), ""); + const msg = builder.buildMessage(monitor, check, makeDecision(), ""); expect(msg.content.details).not.toContainEqual(expect.stringContaining("Response Code:")); expect(msg.content.details).not.toContainEqual(expect.stringContaining("Error:")); @@ -327,7 +314,7 @@ describe("NotificationMessageBuilder", () => { it("includes URL, Status, and Type in details", () => { const monitor = makeMonitor({ status: "down" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision(), ""); expect(msg.content.details).toContain("URL: https://example.com"); expect(msg.content.details).toContain("Status: Down"); @@ -336,13 +323,13 @@ describe("NotificationMessageBuilder", () => { it("sets summary text", () => { const monitor = makeMonitor({ status: "down" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision(), ""); expect(msg.content.summary).toBe('Monitor "Test Monitor" is currently down and unreachable.'); }); it("sets timestamp", () => { const monitor = makeMonitor({ status: "down" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision(), ""); expect(msg.content.timestamp).toBeInstanceOf(Date); }); }); @@ -350,7 +337,7 @@ describe("NotificationMessageBuilder", () => { describe("monitor_up", () => { it("includes recovery details", () => { const monitor = makeMonitor({ status: "up", type: "http" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision(), ""); expect(msg.content.title).toBe("Monitor Recovered: Test Monitor"); expect(msg.content.summary).toBe('Monitor "Test Monitor" is back up and operational.'); @@ -361,11 +348,9 @@ describe("NotificationMessageBuilder", () => { describe("threshold_breach", () => { it("includes threshold breaches in content", () => { const monitor = makeMonitor({ status: "up", type: "hardware", cpuAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.9, temperature: [50] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.9, temperature: [50] } }); - const msg = builder.buildMessage(monitor, response, makeDecision({ notificationReason: "threshold_breach" }), ""); + const msg = builder.buildMessage(monitor, check, makeDecision({ notificationReason: "threshold_breach" }), ""); expect(msg.content.thresholds).toEqual( expect.arrayContaining([ @@ -382,7 +367,7 @@ describe("NotificationMessageBuilder", () => { describe("threshold_resolved", () => { it("includes resolved details", () => { const monitor = makeMonitor({ status: "up", type: "hardware" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision({ notificationReason: "threshold_resolved" }), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision({ notificationReason: "threshold_resolved" }), ""); expect(msg.content.title).toBe("Thresholds Resolved: Test Monitor"); expect(msg.content.summary).toBe('Monitor "Test Monitor" thresholds have returned to normal.'); @@ -392,13 +377,12 @@ describe("NotificationMessageBuilder", () => { describe("container_breach", () => { it("lists one detail line per breaching container", () => { - const response = makeStatusResponse({ - type: "docker", + const check = makeCheck({ status: true, - payload: makeDockerPayload([{ name: "db", state: "exited", exitCode: 1 }, { name: "api", health: "unhealthy" }, { name: "web" }]), - } as any); + containers: makeContainers([{ name: "db", state: "exited", exitCode: 1 }, { name: "api", health: "unhealthy" }, { name: "web" }]), + }); - const msg = builder.buildMessage(makeDockerMonitor(), response, makeDecision({ notificationReason: "threshold_breach" }), ""); + const msg = builder.buildMessage(makeDockerMonitor(), check, makeDecision({ notificationReason: "threshold_breach" }), ""); expect(msg.content.title).toBe("Container Alert: Test Monitor"); expect(msg.content.summary).toBe('2 container(s) on "Test Monitor" need attention.'); @@ -407,25 +391,24 @@ describe("NotificationMessageBuilder", () => { }); it("only lists containers matching an enabled switch", () => { - const response = makeStatusResponse({ - type: "docker", + const check = makeCheck({ status: true, - payload: makeDockerPayload([ + containers: makeContainers([ { name: "db", state: "exited", exitCode: 1 }, { name: "api", health: "unhealthy" }, ]), - } as any); + }); const monitor = makeDockerMonitor({ dockerAlertOnStopped: false }); - const msg = builder.buildMessage(monitor, response, makeDecision({ notificationReason: "threshold_breach" }), ""); + const msg = builder.buildMessage(monitor, check, makeDecision({ notificationReason: "threshold_breach" }), ""); expect(msg.content.details).toEqual(["URL: unix:///var/run/docker.sock", "Type: docker", "api: unhealthy"]); }); - it("lists no containers when the payload is an error string", () => { - const response = makeStatusResponse({ type: "docker", status: false, payload: "connect ECONNREFUSED" } as any); + it("lists no containers when the check carries no container data", () => { + const check = makeCheck({ status: false, message: "connect ECONNREFUSED" }); - const msg = builder.buildMessage(makeDockerMonitor(), response, makeDecision({ notificationReason: "threshold_breach" }), ""); + const msg = builder.buildMessage(makeDockerMonitor(), check, makeDecision({ notificationReason: "threshold_breach" }), ""); expect(msg.content.summary).toBe('0 container(s) on "Test Monitor" need attention.'); expect(msg.content.details).toEqual(["URL: unix:///var/run/docker.sock", "Type: docker"]); @@ -436,7 +419,7 @@ describe("NotificationMessageBuilder", () => { it("includes recovered details", () => { const msg = builder.buildMessage( makeDockerMonitor({ status: "up" }), - makeStatusResponse(), + makeCheck(), makeDecision({ notificationReason: "threshold_resolved" }), "" ); @@ -457,7 +440,7 @@ describe("NotificationMessageBuilder", () => { // Instead, let's verify the default case exists by checking that // non-standard types would get default content const monitor = makeMonitor({ status: "up", type: "http" }); - const msg = builder.buildMessage(monitor, makeStatusResponse(), makeDecision(), ""); + const msg = builder.buildMessage(monitor, makeCheck(), makeDecision(), ""); // monitor_up is handled, so content should be monitor_up specific expect(msg.content.title).toBe("Monitor Recovered: Test Monitor"); @@ -484,7 +467,7 @@ describe("NotificationMessageBuilder", () => { describe("buildContent edge cases", () => { it("returns default content for unhandled notification type", () => { const monitor = makeMonitor({ status: "up", type: "http" }); - const result = (builder as any).buildContent("test", monitor, makeStatusResponse()); + const result = (builder as any).buildContent("test", monitor, makeCheck()); expect(result.title).toBe("Monitor: Test Monitor"); expect(result.summary).toBe('Status update for monitor "Test Monitor".'); @@ -499,27 +482,24 @@ describe("NotificationMessageBuilder", () => { describe("extractThresholdBreaches", () => { it("returns empty array for non-hardware monitor", () => { const monitor = makeMonitor({ type: "http" }); - const response = makeStatusResponse(); + const check = makeCheck({ cpu: { usage_percent: 0.99 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toEqual([]); }); - it("returns empty array when payload is missing", () => { - const monitor = makeMonitor({ type: "hardware" }); - const response = makeStatusResponse({ payload: undefined } as any); + it("returns empty array when the check carries no hardware data", () => { + const monitor = makeMonitor({ + type: "hardware", + cpuAlertThreshold: 80, + memoryAlertThreshold: 80, + diskAlertThreshold: 80, + tempAlertThreshold: 80, + }); + const check = makeCheck(); - const breaches = builder.extractThresholdBreaches(monitor, response); - - expect(breaches).toEqual([]); - }); - - it("returns empty array when hardware data is missing", () => { - const monitor = makeMonitor({ type: "hardware" }); - const response = makeStatusResponse({ payload: { data: undefined } } as any); - - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toEqual([]); }); @@ -529,11 +509,9 @@ describe("NotificationMessageBuilder", () => { describe("cpu threshold", () => { it("detects CPU breach when usage exceeds threshold", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.9, temperature: [50] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.9, temperature: [50] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toContainEqual( expect.objectContaining({ @@ -548,55 +526,45 @@ describe("NotificationMessageBuilder", () => { it("does not report CPU breach when usage is below threshold", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [50] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [50] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "cpu")).toBeUndefined(); }); it("skips CPU check when cpuAlertThreshold is undefined", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: undefined }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.99 } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.99 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "cpu")).toBeUndefined(); }); it("skips CPU check when cpuAlertThreshold is null", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: null as any }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.99 } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.99 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "cpu")).toBeUndefined(); }); it("skips CPU check when cpu usage_percent is undefined", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: undefined } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: undefined } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "cpu")).toBeUndefined(); }); it("skips CPU check when cpu object is undefined", () => { const monitor = makeMonitor({ type: "hardware", cpuAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: undefined as any }), - } as any); + const check = makeCheck({ cpu: undefined }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "cpu")).toBeUndefined(); }); @@ -607,11 +575,9 @@ describe("NotificationMessageBuilder", () => { describe("memory threshold", () => { it("detects memory breach when usage exceeds threshold", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: { usage_percent: 0.85 } as any }), - } as any); + const check = makeCheck({ memory: { usage_percent: 0.85 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toContainEqual( expect.objectContaining({ @@ -626,55 +592,45 @@ describe("NotificationMessageBuilder", () => { it("does not report memory breach when usage is below threshold", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: 90 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: { usage_percent: 0.5 } as any }), - } as any); + const check = makeCheck({ memory: { usage_percent: 0.5 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "memory")).toBeUndefined(); }); it("skips memory check when memoryAlertThreshold is undefined", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: undefined }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: { usage_percent: 0.99 } as any }), - } as any); + const check = makeCheck({ memory: { usage_percent: 0.99 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "memory")).toBeUndefined(); }); it("skips memory check when memoryAlertThreshold is null", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: null as any }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: { usage_percent: 0.99 } as any }), - } as any); + const check = makeCheck({ memory: { usage_percent: 0.99 } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "memory")).toBeUndefined(); }); it("skips memory check when memory usage_percent is undefined", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: { usage_percent: undefined } as any }), - } as any); + const check = makeCheck({ memory: { usage_percent: undefined } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "memory")).toBeUndefined(); }); it("skips memory check when memory object is undefined", () => { const monitor = makeMonitor({ type: "hardware", memoryAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ memory: undefined as any }), - } as any); + const check = makeCheck({ memory: undefined }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "memory")).toBeUndefined(); }); @@ -685,13 +641,9 @@ describe("NotificationMessageBuilder", () => { describe("disk threshold", () => { it("detects disk breach using highest usage across multiple disks", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ - disk: [{ usage_percent: 0.5 }, { usage_percent: 0.95 }, { usage_percent: 0.7 }] as any, - }), - } as any); + const check = makeCheck({ disk: [{ usage_percent: 0.5 }, { usage_percent: 0.95 }, { usage_percent: 0.7 }] }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toContainEqual( expect.objectContaining({ @@ -705,59 +657,45 @@ describe("NotificationMessageBuilder", () => { it("does not report disk breach when all disks are below threshold", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: 90 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ - disk: [{ usage_percent: 0.5 }, { usage_percent: 0.6 }] as any, - }), - } as any); + const check = makeCheck({ disk: [{ usage_percent: 0.5 }, { usage_percent: 0.6 }] }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "disk")).toBeUndefined(); }); it("skips disk check when diskAlertThreshold is undefined", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: undefined }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ disk: [{ usage_percent: 0.99 }] as any }), - } as any); + const check = makeCheck({ disk: [{ usage_percent: 0.99 }] }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "disk")).toBeUndefined(); }); it("skips disk check when diskAlertThreshold is null", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: null as any }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ disk: [{ usage_percent: 0.99 }] as any }), - } as any); + const check = makeCheck({ disk: [{ usage_percent: 0.99 }] }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "disk")).toBeUndefined(); }); it("skips disk check when disk is not an array", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ disk: "not-an-array" as any }), - } as any); + const check = makeCheck({ disk: "not-an-array" as any }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "disk")).toBeUndefined(); }); it("skips disks with undefined usage_percent", () => { const monitor = makeMonitor({ type: "hardware", diskAlertThreshold: 50 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ - disk: [{ usage_percent: undefined }, { usage_percent: 0.3 }] as any, - }), - } as any); + const check = makeCheck({ disk: [{ usage_percent: undefined }, { usage_percent: 0.3 }] }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "disk")).toBeUndefined(); }); @@ -768,11 +706,9 @@ describe("NotificationMessageBuilder", () => { describe("temperature threshold", () => { it("detects temperature breach from array of temperatures", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [65, 75, 68] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [65, 75, 68] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toContainEqual( expect.objectContaining({ @@ -787,11 +723,9 @@ describe("NotificationMessageBuilder", () => { it("handles single temperature value (non-array)", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: 80 } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: 80 as any } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches).toContainEqual( expect.objectContaining({ @@ -804,11 +738,9 @@ describe("NotificationMessageBuilder", () => { it("does not report temperature breach when below threshold", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [60, 65] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [60, 65] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); @@ -820,55 +752,45 @@ describe("NotificationMessageBuilder", () => { // content for a breach some OTHER metric triggered could still list temp as breaching). it("does not report temperature breach when exactly at threshold", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 80 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [80] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [80] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); it("skips temperature check when tempAlertThreshold is undefined", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: undefined }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [99] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [99] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); it("skips temperature check when tempAlertThreshold is null", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: null as any }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: [99] } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: [99] } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); it("skips temperature check when cpu.temperature is falsy", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: { usage_percent: 0.5, temperature: null } as any }), - } as any); + const check = makeCheck({ cpu: { usage_percent: 0.5, temperature: null as any } }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); it("skips temperature check when cpu object is undefined", () => { const monitor = makeMonitor({ type: "hardware", tempAlertThreshold: 70 }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ cpu: undefined as any }), - } as any); + const check = makeCheck({ cpu: undefined }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); expect(breaches.find((b) => b.metric === "temp")).toBeUndefined(); }); @@ -885,15 +807,13 @@ describe("NotificationMessageBuilder", () => { diskAlertThreshold: 85, tempAlertThreshold: 65, }); - const response = makeStatusResponse({ - payload: makeHardwarePayload({ - cpu: { usage_percent: 0.9, temperature: [70] } as any, - memory: { usage_percent: 0.85 } as any, - disk: [{ usage_percent: 0.95 }] as any, - }), - } as any); + const check = makeCheck({ + cpu: { usage_percent: 0.9, temperature: [70] }, + memory: { usage_percent: 0.85 }, + disk: [{ usage_percent: 0.95 }], + }); - const breaches = builder.extractThresholdBreaches(monitor, response); + const breaches = builder.extractThresholdBreaches(monitor, check); const metrics = breaches.map((b) => b.metric); expect(metrics).toContain("cpu"); diff --git a/server/test/unit/services/notificationsService.test.ts b/server/test/unit/services/notificationsService.test.ts index 15ca97e85..7abba9d7f 100644 --- a/server/test/unit/services/notificationsService.test.ts +++ b/server/test/unit/services/notificationsService.test.ts @@ -3,7 +3,7 @@ import { NotificationsService } from "../../../src/domain/notifications/notifica import { createMockLogger } from "../../helpers/createMockLogger.ts"; import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { Notification } from "../../../src/domain/notifications/notification.type.ts"; -import type { MonitorStatusResponse } from "../../../src/types/network.ts"; +import type { Check } from "../../../src/domain/checks/check.type.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.interface.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -130,7 +130,17 @@ const makeDecision = (overrides?: Partial): MonitorAction ...overrides, }); -const makeStatusResponse = () => ({ monitorId: "mon-1", status: false, code: 500 }) as unknown as MonitorStatusResponse; +const makeCheck = (overrides?: Partial): Check => ({ + id: "check-1", + metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, + status: false, + responseTime: 100, + statusCode: 500, + message: "Internal Server Error", + createdAt: "2026-01-01T00:00:00Z", + updatedAt: "2026-01-01T00:00:00Z", + ...overrides, +}); // ── Tests ──────────────────────────────────────────────────────────────────── @@ -140,7 +150,7 @@ describe("NotificationsService", () => { describe("handleNotifications", () => { it("returns false when shouldSendNotification is false", async () => { const { service } = createService(); - const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision({ shouldSendNotification: false })); + const result = await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision({ shouldSendNotification: false })); expect(result).toBe(false); }); @@ -148,7 +158,7 @@ describe("NotificationsService", () => { const { service, notificationsRepository, emailProvider } = createService(); (notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type: "email" })]); - const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + const result = await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); expect(result).toBe(true); expect(emailProvider.sendMessage).toHaveBeenCalledTimes(1); @@ -160,7 +170,7 @@ describe("NotificationsService", () => { const deps = createService(); (deps.notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type })]); - await deps.service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + await deps.service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); const providerMap: Record> = { webhook: deps.webhookProvider, @@ -183,7 +193,7 @@ describe("NotificationsService", () => { const { service, notificationsRepository, logger } = createService(); (notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification({ type: "carrier_pigeon" as any })]); - const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + const result = await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); expect(result).toBe(false); expect(logger.warn).toHaveBeenCalledWith( @@ -197,7 +207,7 @@ describe("NotificationsService", () => { const { service, notificationsRepository, logger } = createService({ notificationMessageBuilder }); (notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification()]); - const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + const result = await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); expect(result).toBe(false); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Notification message not provided" })); @@ -207,7 +217,7 @@ describe("NotificationsService", () => { const { service, notificationsRepository } = createService(); (notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([]); - const result = await service.handleNotifications(makeMonitor({ notifications: undefined as any }), makeStatusResponse(), makeDecision()); + const result = await service.handleNotifications(makeMonitor({ notifications: undefined as any }), makeCheck(), makeDecision()); expect(result).toBe(true); expect(notificationsRepository.findNotificationsByIds).toHaveBeenCalledWith([]); @@ -222,7 +232,7 @@ describe("NotificationsService", () => { emailProvider.sendMessage.mockResolvedValue(true); slackProvider.sendMessage.mockResolvedValue(false); - const result = await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + const result = await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); expect(result).toBe(false); expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("1 success, 1 failure") })); @@ -233,7 +243,7 @@ describe("NotificationsService", () => { const { service, notificationsRepository, notificationMessageBuilder } = createService({ settingsService }); (notificationsRepository.findNotificationsByIds as jest.Mock).mockResolvedValue([makeNotification()]); - await service.handleNotifications(makeMonitor(), makeStatusResponse(), makeDecision()); + await service.handleNotifications(makeMonitor(), makeCheck(), makeDecision()); expect(notificationMessageBuilder.buildMessage).toHaveBeenCalledWith( expect.anything(), diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 393cb5676..50005312d 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -4,7 +4,7 @@ import { createMockLogger } from "../../helpers/createMockLogger.ts"; import { MAX_RECENT_CHECKS } from "../../../src/domain/monitors/monitor.type.ts"; import type { Monitor, MonitorStatus } from "../../../src/domain/monitors/monitor.type.ts"; import type { Check } from "../../../src/domain/checks/check.type.ts"; -import type { MonitorStatusResponse, HardwareStatusPayload, DockerStatusPayload } from "../../../src/types/network.ts"; +import type { DockerContainerInfo } from "../../../src/domain/docker/docker.type.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"; @@ -98,18 +98,6 @@ const makeMonitor = (overrides?: Partial): Monitor => ...overrides, }) as Monitor; -const makeStatusResponse = (overrides?: Partial): MonitorStatusResponse => - ({ - monitorId: "mon-1", - teamId: "team-1", - type: "http", - status: true, - code: 200, - message: "OK", - responseTime: 100, - ...overrides, - }) as MonitorStatusResponse; - const makeCheck = (overrides?: Partial): Check => ({ id: "check-1", @@ -155,7 +143,7 @@ describe("StatusService", () => { const { service, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({}); - const result = await service.updateRunningStats(makeMonitor(), makeStatusResponse({ responseTime: 50 })); + const result = await service.updateRunningStats(makeCheck({ responseTime: 50 }), makeMonitor()); expect(result).toBe(true); expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith( @@ -172,7 +160,7 @@ describe("StatusService", () => { const { service, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({}); - const result = await service.updateRunningStats(makeMonitor(), makeStatusResponse({ status: false, responseTime: 100 })); + const result = await service.updateRunningStats(makeCheck({ status: false, responseTime: 100 }), makeMonitor()); expect(result).toBe(true); expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ status: false, responseTime: 100 })); @@ -182,7 +170,7 @@ describe("StatusService", () => { const { service, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({}); - await service.updateRunningStats(makeMonitor(), makeStatusResponse({ responseTime: undefined })); + await service.updateRunningStats(makeCheck({ responseTime: undefined as any }), makeMonitor()); expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ responseTime: 0 })); }); @@ -191,7 +179,7 @@ describe("StatusService", () => { const { service, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({}); - await service.updateRunningStats(makeMonitor(), makeStatusResponse({ responseTime: 0 })); + await service.updateRunningStats(makeCheck({ responseTime: 0 }), makeMonitor()); expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ responseTime: 0 })); }); @@ -200,7 +188,7 @@ describe("StatusService", () => { const { service, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({}); - await service.updateRunningStats(makeMonitor(), makeStatusResponse({ status: undefined as unknown as boolean })); + await service.updateRunningStats(makeCheck({ status: undefined as any }), makeMonitor()); expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ status: false })); }); @@ -209,7 +197,7 @@ describe("StatusService", () => { const { service, logger, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue(new Error("db write failed")); - const result = await service.updateRunningStats(makeMonitor(), makeStatusResponse()); + const result = await service.updateRunningStats(makeCheck(), makeMonitor()); expect(result).toBe(false); expect(logger.error).toHaveBeenCalledWith( @@ -225,7 +213,7 @@ describe("StatusService", () => { const { service, logger, monitorStatsRepository } = createService(); (monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue("string error"); - const result = await service.updateRunningStats(makeMonitor(), makeStatusResponse()); + const result = await service.updateRunningStats(makeCheck(), makeMonitor()); expect(result).toBe(false); expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined })); @@ -241,7 +229,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(), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.prevStatus).toBe("up"); @@ -252,7 +240,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(monitorsRepository.findById).not.toHaveBeenCalled(); expect(result.prevStatus).toBe("up"); @@ -265,7 +253,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({ status: false }), makeCheck({ status: false }), monitor); + await service.updateMonitorStatus(makeCheck({ status: false }), monitor); // Atomic push should have been called with the correct status and window size expect(monitorsRepository.updateStatusWindowAndChecks).toHaveBeenCalledWith( @@ -286,7 +274,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" }), monitor); + await service.updateMonitorStatus(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" })); @@ -304,7 +292,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -322,7 +310,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("up"); @@ -339,7 +327,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(), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -359,7 +347,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -376,7 +364,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -388,7 +376,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(), monitor); + await service.updateMonitorStatus(makeCheck(), monitor); expect(monitor.statusWindow).toEqual([true]); }); @@ -399,7 +387,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(), monitor); + await service.updateMonitorStatus(makeCheck(), monitor); expect(monitor.recentChecks).toHaveLength(1); }); @@ -409,7 +397,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue(new Error("db error")); - await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor)).rejects.toThrow("Failed to update monitor"); + await expect(service.updateMonitorStatus(makeCheck(), monitor)).rejects.toThrow("Failed to update monitor"); }); it("throws AppError with 'Unknown error' for non-Error exceptions", async () => { @@ -417,7 +405,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue("string error"); - await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor)).rejects.toThrow("Unknown error"); + await expect(service.updateMonitorStatus(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 () => { @@ -435,7 +423,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -453,7 +441,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -475,7 +463,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); }); @@ -493,7 +481,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.monitor.status).toBe("down"); }); @@ -514,7 +502,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); expect(result.statusChanged).toBe(false); // was effectively up already — no notification storm @@ -531,7 +519,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.monitor.status).toBe("down"); expect(result.statusChanged).toBe(true); // down must surface so an incident is opened @@ -548,7 +536,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -565,7 +553,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -580,7 +568,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -596,7 +584,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -611,7 +599,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: false }), monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -631,7 +619,7 @@ describe("StatusService", () => { const { service, monitorsRepository } = createService(); (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); - const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); }); @@ -646,7 +634,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 }), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("down"); @@ -659,7 +647,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(), monitor); + const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor); expect(result.monitor.status).toBe("up"); expect(logger.warn).toHaveBeenCalledWith( @@ -677,7 +665,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(), monitor); + const result = await service.updateMonitorStatus(makeCheck({ statusCode: 201 }), monitor); expect(result.code).toBe(201); expect(result.timestamp).toBeGreaterThan(0); @@ -703,13 +691,12 @@ describe("StatusService", () => { ...overrides, }); - const makeHardwareResponse = (payload: HardwareStatusPayload, overrides?: Partial>) => - makeStatusResponse({ - type: "hardware", + const makeHardwareCheck = (metrics: Pick, overrides?: Partial) => + makeCheck({ status: true, - payload, + ...metrics, ...overrides, - } as any) as MonitorStatusResponse; + }); it("detects CPU threshold breach and decrements counter", async () => { const monitor = makeHardwareMonitor({ cpuAlertCounter: 1 }); @@ -717,8 +704,8 @@ 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 response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.5 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.5 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.cpu).toBe(true); expect(result.thresholdBreaches?.memory).toBe(false); @@ -734,8 +721,8 @@ 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 response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.5 }, memory: { usage_percent: 0.9 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.5 }, memory: { usage_percent: 0.9 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.memory).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -749,10 +736,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [{ usage_percent: 0.95 }], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.disk).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -766,10 +751,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1, temperature: [90] }, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.temp).toBe(true); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; @@ -788,10 +771,13 @@ 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 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(), monitor); + const check = makeHardwareCheck({ + cpu: { usage_percent: 0.1, temperature: [30] }, + memory: { usage_percent: 0.1 }, + disk: [{ usage_percent: 0.1 }], + host: {}, + }); + await service.updateMonitorStatus(check, monitor); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; expect(patch.cpuAlertCounter).toBe(5); @@ -809,8 +795,8 @@ 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 response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.monitor.status).toBe("breached"); expect(result.statusChanged).toBe(false); @@ -822,10 +808,13 @@ 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 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(), monitor); + const check = makeHardwareCheck({ + cpu: { usage_percent: 0.1, temperature: [30] }, + memory: { usage_percent: 0.1 }, + disk: [{ usage_percent: 0.1 }], + host: {}, + }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("up"); @@ -844,11 +833,8 @@ 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 response = makeHardwareResponse({ - 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 }), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, { status: false }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.monitor.status).toBe("down"); }); @@ -859,8 +845,8 @@ 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 response = makeHardwareResponse({ data: { cpu: {}, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: {}, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.cpu).toBe(false); }); @@ -871,8 +857,8 @@ 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 response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.1 }, memory: {}, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: {}, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.memory).toBe(false); }); @@ -883,10 +869,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1, temperature: [] }, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.temp).toBe(false); }); @@ -897,36 +881,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)); - 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.temp).toBe(false); }); - it("skips threshold evaluation when payload is undefined", async () => { - const monitor = makeHardwareMonitor(); + it("reports no breaches when the check carries no hardware metrics", async () => { + // A stored hardware check with no metrics (cpu/memory/disk/host all absent) must not + // throw and must not report any breach. + const monitor = makeHardwareMonitor({ cpuAlertCounter: 1 }); 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)); - const response = makeHardwareResponse(undefined as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const result = await service.updateMonitorStatus(makeCheck(), monitor); - expect(result.thresholdBreaches).toBeUndefined(); - }); - - it("skips threshold evaluation when payload.data is undefined", async () => { - const monitor = makeHardwareMonitor(); - 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)); - - const response = makeHardwareResponse({} as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); - - expect(result.thresholdBreaches).toBeUndefined(); + expect(result.thresholdBreaches).toEqual({ cpu: false, memory: false, disk: false, temp: false }); + expect(result.statusChanged).toBe(false); + expect(result.monitor.status).toBe("up"); }); it("does not set thresholdBreaches for non-hardware monitors", async () => { @@ -939,7 +912,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(), monitor); + const result = await service.updateMonitorStatus(makeCheck(), monitor); expect(result.thresholdBreaches).toBeUndefined(); }); @@ -950,10 +923,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [null as any], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -964,10 +935,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [{ device: "/dev/sda" }], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -978,10 +947,8 @@ 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 response = makeHardwareResponse({ - data: { cpu: undefined, memory: { usage_percent: 0.1 }, disk: [], host: {} }, - } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: undefined, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.cpu).toBe(false); expect(result.thresholdBreaches?.temp).toBe(false); @@ -993,10 +960,8 @@ 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 response = makeHardwareResponse({ - data: { cpu: { usage_percent: 0.1 }, memory: undefined, disk: [], host: {} }, - } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: undefined, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.memory).toBe(false); }); @@ -1007,10 +972,8 @@ 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 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(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: undefined, host: {} }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.thresholdBreaches?.disk).toBe(false); }); @@ -1023,8 +986,8 @@ 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 response = makeHardwareResponse({ data: { cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} } } as any); - const result = await service.updateMonitorStatus(response, makeCheck(), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.9 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }); + const result = await service.updateMonitorStatus(check, monitor); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; expect(patch.cpuAlertCounter).toBe(2); @@ -1046,11 +1009,8 @@ 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 response = makeHardwareResponse({ - 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 }), monitor); + const check = makeHardwareCheck({ cpu: { usage_percent: 0.1 }, memory: { usage_percent: 0.1 }, disk: [], host: {} }, { status: false }); + const result = await service.updateMonitorStatus(check, monitor); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -1067,10 +1027,13 @@ 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 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(), monitor); + const check = makeHardwareCheck({ + cpu: { usage_percent: 0.9, temperature: [90] }, + memory: { usage_percent: 0.9 }, + disk: [{ usage_percent: 0.95 }], + host: {}, + }); + await service.updateMonitorStatus(check, monitor); expect(monitor.cpuAlertCounter).toBe(0); expect(monitor.memoryAlertCounter).toBe(0); @@ -1099,8 +1062,8 @@ describe("StatusService", () => { ...overrides, }); - const makeContainers = (containers: Partial[]): DockerStatusPayload => ({ - containers: containers.map((container, index) => ({ + const makeContainers = (containers: Partial[]): DockerContainerInfo[] => + containers.map((container, index) => ({ id: `container-${index}`, name: `container-${index}`, image: "nginx:latest", @@ -1108,23 +1071,20 @@ describe("StatusService", () => { status: "Up 3 hours", health: "none", ...container, - })), - summary: { total: containers.length, running: 0, stopped: 0, unhealthy: 0 }, - }); + })); - const makeDockerResponse = (payload: DockerStatusPayload | null, overrides?: Partial>) => - makeStatusResponse({ - type: "docker", + const makeDockerCheck = (containers: DockerContainerInfo[] | null, overrides?: Partial) => + makeCheck({ status: true, - payload, + ...(containers ? { containers } : {}), ...overrides, - } as any) as MonitorStatusResponse; + }); - const run = async (monitor: Monitor, response: MonitorStatusResponse, check = makeCheck()) => { + const run = async (monitor: Monitor, check: Check) => { 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)); - const result = await service.updateMonitorStatus(response, check, monitor); + const result = await service.updateMonitorStatus(check, monitor); return { result, monitorsRepository }; }; @@ -1132,14 +1092,14 @@ describe("StatusService", () => { const unhealthy = { name: "api", health: "unhealthy" as const }; it("goes to breached on the first check with a stopped container", async () => { - const { result } = await run(makeDockerMonitor(), makeDockerResponse(makeContainers([stopped]))); + const { result } = await run(makeDockerMonitor(), makeDockerCheck(makeContainers([stopped]))); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("breached"); }); it("goes to breached on the first check with an unhealthy container", async () => { - const { result } = await run(makeDockerMonitor(), makeDockerResponse(makeContainers([unhealthy]))); + const { result } = await run(makeDockerMonitor(), makeDockerCheck(makeContainers([unhealthy]))); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("breached"); @@ -1147,7 +1107,7 @@ describe("StatusService", () => { it("stays up on a stopped container when both switches are off", async () => { const monitor = makeDockerMonitor({ dockerAlertOnStopped: false, dockerAlertOnUnhealthy: false }); - const { result } = await run(monitor, makeDockerResponse(makeContainers([stopped, unhealthy]))); + const { result } = await run(monitor, makeDockerCheck(makeContainers([stopped, unhealthy]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -1155,35 +1115,35 @@ describe("StatusService", () => { it("stays up on a stopped container when only the unhealthy switch is on", async () => { const monitor = makeDockerMonitor({ dockerAlertOnStopped: false }); - const { result } = await run(monitor, makeDockerResponse(makeContainers([stopped]))); + const { result } = await run(monitor, makeDockerCheck(makeContainers([stopped]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); }); it("stays up on an exited container with code 0", async () => { - const { result } = await run(makeDockerMonitor(), makeDockerResponse(makeContainers([{ name: "job", state: "exited", exitCode: 0 }]))); + const { result } = await run(makeDockerMonitor(), makeDockerCheck(makeContainers([{ name: "job", state: "exited", exitCode: 0 }]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); }); it("stays breached without a transition while a container is still down", async () => { - const { result } = await run(makeDockerMonitor({ status: "breached" }), makeDockerResponse(makeContainers([stopped]))); + const { result } = await run(makeDockerMonitor({ status: "breached" }), makeDockerCheck(makeContainers([stopped]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("breached"); }); it("stays up without a transition while every container is clean", async () => { - const { result } = await run(makeDockerMonitor(), makeDockerResponse(makeContainers([{ name: "web" }]))); + const { result } = await run(makeDockerMonitor(), makeDockerCheck(makeContainers([{ name: "web" }]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); }); it("recovers from breached to up on the first clean check", async () => { - const { result } = await run(makeDockerMonitor({ status: "breached" }), makeDockerResponse(makeContainers([{ name: "web" }]))); + const { result } = await run(makeDockerMonitor({ status: "breached" }), makeDockerCheck(makeContainers([{ name: "web" }]))); expect(result.statusChanged).toBe(true); expect(result.prevStatus).toBe("breached"); @@ -1192,7 +1152,7 @@ describe("StatusService", () => { it("recovers from breached to up when the breaching switch is turned off", async () => { const monitor = makeDockerMonitor({ status: "breached", dockerAlertOnStopped: false }); - const { result } = await run(monitor, makeDockerResponse(makeContainers([stopped]))); + const { result } = await run(monitor, makeDockerCheck(makeContainers([stopped]))); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("up"); @@ -1200,16 +1160,16 @@ describe("StatusService", () => { it("does not override down with breached when the host is unreachable", async () => { const monitor = makeDockerMonitor({ statusWindow: [false, false, false, false], statusWindowThreshold: 80 }); - const response = makeDockerResponse(null, { status: false, code: 5000 }); - const { result } = await run(monitor, response, makeCheck({ status: false })); + const check = makeDockerCheck(null, { status: false, statusCode: 5000 }); + const { result } = await run(monitor, check); expect(result.monitor.status).toBe("down"); }); it("transitions from breached to down when the reachability threshold trips", async () => { const monitor = makeDockerMonitor({ status: "breached", statusWindow: [false, false, false, false], statusWindowThreshold: 80 }); - const response = makeDockerResponse(null, { status: false, code: 5000 }); - const { result } = await run(monitor, response, makeCheck({ status: false })); + const check = makeDockerCheck(null, { status: false, statusCode: 5000 }); + const { result } = await run(monitor, check); expect(result.statusChanged).toBe(true); expect(result.monitor.status).toBe("down"); @@ -1217,7 +1177,7 @@ describe("StatusService", () => { it("does not evaluate containers while the status window is short", async () => { const monitor = makeDockerMonitor({ statusWindow: [true], statusWindowSize: 5 }); - const { result } = await run(monitor, makeDockerResponse(makeContainers([stopped]))); + const { result } = await run(monitor, makeDockerCheck(makeContainers([stopped]))); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("up"); @@ -1225,15 +1185,15 @@ describe("StatusService", () => { it("does not evaluate a failed check with a null payload", async () => { const monitor = makeDockerMonitor({ status: "breached" }); - const response = makeDockerResponse(null, { status: false, code: 5000 }); - const { result } = await run(monitor, response, makeCheck({ status: false })); + const check = makeDockerCheck(null, { status: false, statusCode: 5000 }); + const { result } = await run(monitor, check); expect(result.statusChanged).toBe(false); expect(result.monitor.status).toBe("breached"); }); it("does not touch the hardware counters or thresholdBreaches", async () => { - const { result, monitorsRepository } = await run(makeDockerMonitor(), makeDockerResponse(makeContainers([stopped]))); + const { result, monitorsRepository } = await run(makeDockerMonitor(), makeDockerCheck(makeContainers([stopped]))); expect(result.thresholdBreaches).toBeUndefined(); const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6]; diff --git a/server/test/unit/services/workerPipeline.test.ts b/server/test/unit/services/workerPipeline.test.ts index 0b168ef24..2e1335f88 100644 --- a/server/test/unit/services/workerPipeline.test.ts +++ b/server/test/unit/services/workerPipeline.test.ts @@ -92,7 +92,6 @@ const createPipeline = (overrides?: Record) => { }, checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }), - toStatusResponse: jest.fn().mockReturnValue({ monitorId: "m1", status: true, code: 200, message: "OK" }), createChecks: jest.fn().mockResolvedValue([]), }, networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) }, @@ -624,7 +623,7 @@ describe("WorkerPipeline", () => { it("loads the row's pending checks, dispatches each, and pulls its id after dispatch", async () => { const job = makeEvaluateJob("c1"); const { pipeline, deps } = createPipeline({ - checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([{ id: "c1" }]) }, + checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([makeCheck({ id: "c1" })]) }, }); await pipeline.handleEvaluate(job); @@ -661,7 +660,9 @@ describe("WorkerPipeline", () => { it("stops applying checks once the lease is lost so a second claimer does not double-apply", async () => { const job = makeEvaluateJob("c1", "c2", "c3"); const { pipeline, deps } = createPipeline({ - checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([{ id: "c1" }, { id: "c2" }, { id: "c3" }]) }, + checksRepository: { + findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([makeCheck({ id: "c1" }), makeCheck({ id: "c2" }), makeCheck({ id: "c3" })]), + }, jobsRepository: { upsertEvaluate: jest.fn(), pullEvaluated: jest.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false), // lease lost after c2 @@ -679,7 +680,7 @@ describe("WorkerPipeline", () => { it("pulls ids whose check no longer exists so they do not sit on the row forever", async () => { const job = makeEvaluateJob("c1", "c-gone"); const { pipeline, deps } = createPipeline({ - checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([{ id: "c1" }]) }, // c-gone was cleaned up + checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([makeCheck({ id: "c1" })]) }, // c-gone was cleaned up }); await pipeline.handleEvaluate(job); @@ -695,7 +696,7 @@ describe("WorkerPipeline", () => { const postWriteMonitor = makeMonitor({ status: "down" }); // what updateStatusWindowAndChecks would return after check c1 const { pipeline, deps } = createPipeline({ monitorsRepository: { findByIdLean: jest.fn().mockResolvedValue(initialMonitor), updateById: jest.fn() }, - checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([{ id: "c1" }, { id: "c2" }]) }, + checksRepository: { findUnevaluatedByMonitorId: jest.fn().mockResolvedValue([makeCheck({ id: "c1" }), makeCheck({ id: "c2" })]) }, statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(makeStatusChange({ monitor: postWriteMonitor })) }, }); @@ -705,36 +706,33 @@ describe("WorkerPipeline", () => { expect(deps.monitorsRepository.findByIdLean).toHaveBeenCalledTimes(1); expect(deps.statusService.updateMonitorStatus).toHaveBeenCalledTimes(2); // First check evaluates against the freshly-read monitor; the second against the post-write monitor from the first - expect(deps.statusService.updateMonitorStatus.mock.calls[0][2]).toBe(initialMonitor); - expect(deps.statusService.updateMonitorStatus.mock.calls[1][2]).toBe(postWriteMonitor); + expect(deps.statusService.updateMonitorStatus.mock.calls[0][1]).toBe(initialMonitor); + expect(deps.statusService.updateMonitorStatus.mock.calls[1][1]).toBe(postWriteMonitor); }); }); describe("evaluateCheck", () => { - it("threads status, check, and monitor through to the status service", async () => { + it("threads check and monitor through to the status service", async () => { const { pipeline, deps } = createPipeline(); - const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; - const check = { id: "check-1" } as any; + const check = makeCheck({ status: true, statusCode: 200, message: "OK" }); const monitor = makeMonitor({ status: "up" }); - await pipeline.evaluateCheck(status, check, monitor); + await pipeline.evaluateCheck(check, monitor); - expect(deps.statusService.updateMonitorStatus).toHaveBeenCalledWith(status, check, monitor); + expect(deps.statusService.updateMonitorStatus).toHaveBeenCalledWith(check, monitor); }); it("returns an evaluation that opens an incident on a down transition", async () => { - const status = { monitorId: "m1", status: false, code: 500, message: "Error" } as any; - const check = { id: "check-1" } as any; + const check = makeCheck({ status: false, statusCode: 500, message: "Error" }); const statusChange = makeStatusChange({ monitor: makeMonitor({ status: "down" }), statusChanged: true, prevStatus: "up", code: 500 }); const { pipeline } = createPipeline({ statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, }); - const result = await pipeline.evaluateCheck(status, check, makeMonitor()); + const result = await pipeline.evaluateCheck(check, makeMonitor()); expect(result).toMatchObject({ monitor: statusChange.monitor, - status, check, statusChange, decision: expect.objectContaining({ @@ -746,22 +744,20 @@ describe("WorkerPipeline", () => { }); it("returns an evaluation that resolves an incident on recovery", async () => { - const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; const statusChange = makeStatusChange({ monitor: makeMonitor({ status: "up" }), statusChanged: true, prevStatus: "down" }); const { pipeline } = createPipeline({ statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, }); - const result = await pipeline.evaluateCheck(status, { id: "check-1" } as any, makeMonitor({ status: "down" })); + const result = await pipeline.evaluateCheck(makeCheck({ status: true, statusCode: 200, message: "OK" }), makeMonitor({ status: "down" })); expect(result.decision).toMatchObject({ shouldResolveIncident: true, shouldSendNotification: true }); }); it("produces a no-op decision when status did not change", async () => { - const status = { monitorId: "m1", status: true, code: 200, message: "OK" } as any; const { pipeline } = createPipeline(); - const result = await pipeline.evaluateCheck(status, { id: "check-1" } as any, makeMonitor()); + const result = await pipeline.evaluateCheck(makeCheck({ status: true, statusCode: 200, message: "OK" }), makeMonitor()); expect(result.decision).toMatchObject({ shouldCreateIncident: false, @@ -773,12 +769,11 @@ describe("WorkerPipeline", () => { // ── degraded egress ─────────────────────────────────────────────────── it("short-circuits a check flagged with degraded egress without touching monitor status", async () => { - const status = { monitorId: "m1", status: false, code: 500, message: "Error" } as any; - const check = { id: "check-1", egressStatus: "degraded" } as any; + const check = makeCheck({ status: false, statusCode: 500, message: "Error", egressStatus: "degraded" }); const monitor = makeMonitor({ status: "up" }); const { pipeline, deps } = createPipeline(); - const result = await pipeline.evaluateCheck(status, check, monitor); + const result = await pipeline.evaluateCheck(check, monitor); expect(deps.statusService.updateMonitorStatus).not.toHaveBeenCalled(); expect(result.monitor).toBe(monitor); @@ -795,13 +790,12 @@ describe("WorkerPipeline", () => { }); it("evaluates a check flagged with egress ok as normal", async () => { - const status = { monitorId: "m1", status: false, code: 500, message: "Error" } as any; - const check = { id: "check-1", egressStatus: "ok" } as any; + const check = makeCheck({ status: false, statusCode: 500, message: "Error", egressStatus: "ok" }); const { pipeline, deps } = createPipeline(); - await pipeline.evaluateCheck(status, check, makeMonitor()); + await pipeline.evaluateCheck(check, makeMonitor()); - expect(deps.statusService.updateMonitorStatus).toHaveBeenCalledWith(status, check, expect.anything()); + expect(deps.statusService.updateMonitorStatus).toHaveBeenCalledWith(check, expect.anything()); }); }); @@ -812,11 +806,7 @@ describe("WorkerPipeline", () => { const { pipeline } = createPipeline({ statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) }, }); - const result = await pipeline.evaluateCheck( - { monitorId: "m1", status: true, code: 200, message: "OK" } as any, - { id: "check-1" } as any, - makeMonitor() - ); + const result = await pipeline.evaluateCheck(makeCheck({ status: true, statusCode: 200, message: "OK" }), makeMonitor()); return result.decision; };