refactor out rebuilding of status response
This commit is contained in:
1 parent
849ea26cf9
commit
0d75629dcb
18 files changed
+436
-786
No files matched your search
@@ -13,7 +13,6 @@ const SERVICE_NAME = "checkService";
|
||||
export interface ICheckService {
|
||||
createChecks(checks: Check[]): Promise<Check[]>;
|
||||
toCheck(statusResponse: MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>): Check | undefined;
|
||||
toStatusResponse(check: Check): MonitorStatusResponse<MonitorPayloadMap[keyof MonitorPayloadMap]>;
|
||||
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<MonitorPayloadMap[keyof MonitorPayloadMap]> = {
|
||||
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,
|
||||
|
||||
@@ -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 && {
|
||||
|
||||
@@ -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<Incident | null>;
|
||||
handleIncident(monitor: Monitor, code: number, decision: MonitorActionDecision, check: Check): Promise<Incident | null>;
|
||||
resolveIncident(incidentId: string, userId: string, teamId: string, comment?: string, userEmail?: string): Promise<Incident>;
|
||||
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<Incident | null> => {
|
||||
handleIncident = async (monitor: Monitor, code: number, decision: MonitorActionDecision, check: Check): Promise<Incident | null> => {
|
||||
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";
|
||||
|
||||
@@ -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<HardwareStatusPayload>);
|
||||
return this.buildThresholdBreachContent(monitor, check);
|
||||
case "threshold_resolved":
|
||||
return this.buildThresholdResolvedContent(monitor);
|
||||
case "container_breach":
|
||||
return this.buildContainerBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse<DockerStatusPayload>);
|
||||
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<HardwareStatusPayload>): 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<DockerStatusPayload>): 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<HardwareStatusPayload>): 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) {
|
||||
|
||||
@@ -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<NotificationChannel, INotificationProvider>;
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface INotificationsService {
|
||||
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
|
||||
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
|
||||
deleteById: (id: string, teamId: string) => Promise<Notification>;
|
||||
handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => Promise<boolean>;
|
||||
handleNotifications: (monitor: Monitor, check: Check, decision: MonitorActionDecision) => Promise<boolean>;
|
||||
|
||||
sendTestNotification: (notification: Partial<Notification>) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
@@ -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<boolean> => {
|
||||
@@ -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<Notification>) => {
|
||||
|
||||
@@ -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<HardwareMetricKey, boolean>;
|
||||
type HardwareCounters = Record<HardwareMetricKey, number>;
|
||||
|
||||
export interface IStatusService {
|
||||
updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse): Promise<boolean>;
|
||||
updateMonitorStatus(
|
||||
statusResponse: MonitorStatusResponse<
|
||||
| PingStatusPayload
|
||||
| HttpStatusPayload
|
||||
| PageSpeedStatusPayload
|
||||
| HardwareStatusPayload
|
||||
| DockerStatusPayload
|
||||
| PortStatusPayload
|
||||
| GameStatusPayload
|
||||
| GrpcStatusPayload
|
||||
| undefined
|
||||
>,
|
||||
check: Check,
|
||||
monitor: Monitor
|
||||
): Promise<StatusChangeResult>;
|
||||
updateRunningStats(check: Check, monitor: Monitor): Promise<boolean>;
|
||||
updateMonitorStatus(check: Check, monitor: Monitor): Promise<StatusChangeResult>;
|
||||
}
|
||||
|
||||
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<StatusChangeResult> => {
|
||||
updateMonitorStatus = async (check: Check, monitor: Monitor): Promise<StatusChangeResult> => {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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<MonitorPayloadMap[keyof MonitorPayloadMap]>; // raw result from networkService.requestStatus
|
||||
check: Check;
|
||||
statusChange: StatusChangeResult; // from statusService.updateMonitorStatus
|
||||
decision: MonitorActionDecision;
|
||||
|
||||
@@ -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<MonitorEvaluation> => {
|
||||
evaluateCheck = async (check: Check, monitor: Monitor): Promise<MonitorEvaluation> => {
|
||||
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 => {
|
||||
|
||||
@@ -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>): 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);
|
||||
};
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("Evaluate cursor with more than one processing worker", () => {
|
||||
bufferService,
|
||||
dockerLogsService: { buildDockerLogs: jest.fn<any>() } 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 };
|
||||
},
|
||||
|
||||
@@ -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>): 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<HardwareStatusPayload>);
|
||||
const fromRebuilt = builder.extractThresholdBreaches(monitor, rebuilt as MonitorStatusResponse<HardwareStatusPayload>);
|
||||
|
||||
// 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", () => {
|
||||
|
||||
@@ -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>): Incident =>
|
||||
...overrides,
|
||||
}) as Incident;
|
||||
|
||||
const makeCheck = (overrides?: Partial<Check>): 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>): 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();
|
||||
});
|
||||
|
||||
@@ -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<MonitorActionDecision>): MonitorAction
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeStatusResponse = (overrides?: Partial<MonitorStatusResponse>): 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>): 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"]>): 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"][number]>[]): DockerStatusPayload => ({
|
||||
containers: containers.map((container, index) => ({
|
||||
const makeContainers = (containers: Partial<DockerContainerInfo>[]): DockerContainerInfo[] =>
|
||||
containers.map((container, index) => ({
|
||||
id: `container-${index}`,
|
||||
name: `container-${index}`,
|
||||
image: "nginx:latest",
|
||||
@@ -60,9 +52,7 @@ const makeDockerPayload = (containers: Partial<DockerStatusPayload["containers"]
|
||||
status: "Up 3 hours",
|
||||
health: "none",
|
||||
...container,
|
||||
})),
|
||||
summary: { total: containers.length, running: 0, stopped: 0, unhealthy: 0 },
|
||||
});
|
||||
}));
|
||||
|
||||
const makeDockerMonitor = (overrides?: Partial<Monitor>): 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");
|
||||
|
||||
@@ -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<MonitorActionDecision>): MonitorAction
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const makeStatusResponse = () => ({ monitorId: "mon-1", status: false, code: 500 }) as unknown as MonitorStatusResponse;
|
||||
const makeCheck = (overrides?: Partial<Check>): 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<string, ReturnType<typeof createProvider>> = {
|
||||
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(),
|
||||
|
||||
@@ -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>): Monitor =>
|
||||
...overrides,
|
||||
}) as Monitor;
|
||||
|
||||
const makeStatusResponse = (overrides?: Partial<MonitorStatusResponse>): 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>): 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<MonitorStatusResponse<HardwareStatusPayload>>) =>
|
||||
makeStatusResponse({
|
||||
type: "hardware",
|
||||
const makeHardwareCheck = (metrics: Pick<Check, "cpu" | "memory" | "disk" | "host">, overrides?: Partial<Check>) =>
|
||||
makeCheck({
|
||||
status: true,
|
||||
payload,
|
||||
...metrics,
|
||||
...overrides,
|
||||
} as any) as MonitorStatusResponse<HardwareStatusPayload>;
|
||||
});
|
||||
|
||||
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"][number]>[]): DockerStatusPayload => ({
|
||||
containers: containers.map((container, index) => ({
|
||||
const makeContainers = (containers: Partial<DockerContainerInfo>[]): 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<MonitorStatusResponse<DockerStatusPayload>>) =>
|
||||
makeStatusResponse({
|
||||
type: "docker",
|
||||
const makeDockerCheck = (containers: DockerContainerInfo[] | null, overrides?: Partial<Check>) =>
|
||||
makeCheck({
|
||||
status: true,
|
||||
payload,
|
||||
...(containers ? { containers } : {}),
|
||||
...overrides,
|
||||
} as any) as MonitorStatusResponse<DockerStatusPayload>;
|
||||
});
|
||||
|
||||
const run = async (monitor: Monitor, response: MonitorStatusResponse<DockerStatusPayload>, 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];
|
||||
|
||||
@@ -92,7 +92,6 @@ const createPipeline = (overrides?: Record<string, any>) => {
|
||||
},
|
||||
checkService: {
|
||||
toCheck: jest.fn<any>().mockReturnValue({ id: "check-1" }),
|
||||
toStatusResponse: jest.fn<any>().mockReturnValue({ monitorId: "m1", status: true, code: 200, message: "OK" }),
|
||||
createChecks: jest.fn<any>().mockResolvedValue([]),
|
||||
},
|
||||
networkService: { requestStatus: jest.fn<any>().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<any>().mockResolvedValue([{ id: "c1" }]) },
|
||||
checksRepository: { findUnevaluatedByMonitorId: jest.fn<any>().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<any>().mockResolvedValue([{ id: "c1" }, { id: "c2" }, { id: "c3" }]) },
|
||||
checksRepository: {
|
||||
findUnevaluatedByMonitorId: jest.fn<any>().mockResolvedValue([makeCheck({ id: "c1" }), makeCheck({ id: "c2" }), makeCheck({ id: "c3" })]),
|
||||
},
|
||||
jobsRepository: {
|
||||
upsertEvaluate: jest.fn<any>(),
|
||||
pullEvaluated: jest.fn<any>().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<any>().mockResolvedValue([{ id: "c1" }]) }, // c-gone was cleaned up
|
||||
checksRepository: { findUnevaluatedByMonitorId: jest.fn<any>().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<any>().mockResolvedValue(initialMonitor), updateById: jest.fn<any>() },
|
||||
checksRepository: { findUnevaluatedByMonitorId: jest.fn<any>().mockResolvedValue([{ id: "c1" }, { id: "c2" }]) },
|
||||
checksRepository: { findUnevaluatedByMonitorId: jest.fn<any>().mockResolvedValue([makeCheck({ id: "c1" }), makeCheck({ id: "c2" })]) },
|
||||
statusService: { updateMonitorStatus: jest.fn<any>().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<any>().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<any>().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<any>().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;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user