From 5d77b8804f267f0e6590854d9bd24dc261f203c4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 24 Sep 2026 14:22:31 -0700 Subject: [PATCH] update tests --- server/test/helpers/heartbeatTestHarness.ts | 4 +- .../heartbeatJobThresholdBreach.test.ts | 4 +- .../integration/incidentLifecycle.test.ts | 7 +- .../unit/services/incidentService.test.ts | 67 ++----------------- .../notificationMessageBuilder.test.ts | 47 +++++++++++++ .../services/notificationsService.test.ts | 2 +- 6 files changed, 59 insertions(+), 72 deletions(-) diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index d1166f238..c0f7f1529 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -63,7 +63,7 @@ export interface HeartbeatTestHarness { networkService: { requestStatus: jest.Mock }; bufferStub: { addToBuffer: jest.Mock; addGeoCheckToBuffer: jest.Mock; scheduleNextFlush: jest.Mock }; maintenanceWindowsRepo: { findByMonitorId: jest.Mock }; - messageBuilder: { extractThresholdBreaches: jest.Mock }; + messageBuilder: { buildThresholdBreachMessage: jest.Mock }; egressService: { assessAfterFailure: jest.Mock }; heartbeatJob: (monitor: Monitor) => Promise; setNextResponse: (status: boolean, code: number) => void; @@ -79,7 +79,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness { const statusService = new StatusService(logger, monitorsRepo as any, createStubMonitorStatsRepo() as any); - const messageBuilder = { extractThresholdBreaches: jest.fn().mockReturnValue([]) }; + const messageBuilder = { buildThresholdBreachMessage: jest.fn().mockReturnValue("") }; const incidentService = new IncidentService(logger, incidentsRepo, monitorsRepo as any, { findById: jest.fn() } as any, messageBuilder as any); const notificationsService = { handleNotifications: jest.fn().mockResolvedValue(true) }; diff --git a/server/test/integration/heartbeatJobThresholdBreach.test.ts b/server/test/integration/heartbeatJobThresholdBreach.test.ts index ccb4065fa..2615c030b 100644 --- a/server/test/integration/heartbeatJobThresholdBreach.test.ts +++ b/server/test/integration/heartbeatJobThresholdBreach.test.ts @@ -49,7 +49,7 @@ describe("Heartbeat job: hardware threshold breach", () => { h.setNextResponseFull(makeHardwareResponse(0.9)); // Configure message builder for when incident is created - h.messageBuilder.extractThresholdBreaches.mockReturnValue([{ metric: "cpu", formattedValue: "90%", threshold: 80, unit: "%" }]); + h.messageBuilder.buildThresholdBreachMessage.mockReturnValue("CPU: 90% (threshold: 80%)"); // 4 heartbeats: counter goes 5→4→3→2→1, status still "up" for (let i = 0; i < 4; i++) { @@ -191,7 +191,7 @@ describe("Heartbeat job: hardware threshold breach", () => { h.monitorsRepo.seed(monitor); h.setNextResponseFull(makeHardwareResponse(0.9)); - h.messageBuilder.extractThresholdBreaches.mockReturnValue([{ metric: "cpu", formattedValue: "90%", threshold: 80, unit: "%" }]); + h.messageBuilder.buildThresholdBreachMessage.mockReturnValue("CPU: 90% (threshold: 80%)"); // 5 heartbeats to trigger breach, then 5 more while still breaching for (let i = 0; i < 10; i++) { diff --git a/server/test/integration/incidentLifecycle.test.ts b/server/test/integration/incidentLifecycle.test.ts index ff2ff2c53..64e723183 100644 --- a/server/test/integration/incidentLifecycle.test.ts +++ b/server/test/integration/incidentLifecycle.test.ts @@ -55,7 +55,7 @@ describe("Incident lifecycle (integration)", () => { repo = new InMemoryIncidentsRepository(); monitorsRepo = { findById: jest.fn() } as unknown as jest.Mocked; usersRepo = { findById: jest.fn() } as unknown as jest.Mocked; - messageBuilder = { extractThresholdBreaches: jest.fn() } as unknown as jest.Mocked; + messageBuilder = { buildThresholdBreachMessage: jest.fn() } as unknown as jest.Mocked; service = new IncidentService(createMockLogger() as any, repo, monitorsRepo, usersRepo, messageBuilder); }); @@ -152,10 +152,7 @@ describe("Incident lifecycle (integration)", () => { it("creates a threshold breach incident with statusCode 9999 and descriptive message", async () => { const monitor = makeMonitor({ status: "breached", type: "hardware" }); - (messageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([ - { metric: "cpu", formattedValue: "92%", threshold: 80, unit: "%" }, - { metric: "memory", formattedValue: "88%", threshold: 85, unit: "%" }, - ]); + (messageBuilder.buildThresholdBreachMessage as jest.Mock).mockReturnValue("CPU: 92% (threshold: 80%), MEMORY: 88% (threshold: 85%)"); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); const incident = await service.handleIncident(monitor, decision, makeCheck(200)); diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index fa1edea07..8639542c4 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.ts @@ -36,7 +36,7 @@ const createUsersRepo = () => const createMessageBuilder = () => ({ - extractThresholdBreaches: jest.fn(), + buildThresholdBreachMessage: jest.fn(), }) as unknown as jest.Mocked; const createService = (overrides?: { @@ -131,7 +131,7 @@ describe("IncidentService", () => { it("creates a new incident when shouldCreateIncident and no active incident exists", async () => { const created = makeIncident(); - const { service, incidentsRepository } = createService(); + const { service, incidentsRepository, notificationMessageBuilder } = createService(); (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); (incidentsRepository.create as jest.Mock).mockResolvedValue(created); @@ -150,6 +150,7 @@ describe("IncidentService", () => { statusCode: 500, }) ); + expect(notificationMessageBuilder.buildThresholdBreachMessage).not.toHaveBeenCalled(); }); it("uses status code 9999 and builds message for threshold_breach incidents", async () => { @@ -157,9 +158,7 @@ describe("IncidentService", () => { const { service, incidentsRepository, notificationMessageBuilder } = createService(); (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - (notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([ - { metric: "cpu", formattedValue: "95%", threshold: 80, unit: "%" }, - ]); + (notificationMessageBuilder.buildThresholdBreachMessage as jest.Mock).mockReturnValue("CPU: 95% (threshold: 80%)"); const monitor = makeMonitor({ type: "hardware" }); const check = makeCheck({ metadata: { monitorId: "mon-1", teamId: "team-1", type: "hardware" }, cpu: { usage_percent: 0.95 } }); @@ -167,7 +166,7 @@ describe("IncidentService", () => { const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach", thresholdBreaches }); await service.handleIncident(monitor, decision, check); - expect(notificationMessageBuilder.extractThresholdBreaches).toHaveBeenCalledWith(monitor, check, thresholdBreaches); + expect(notificationMessageBuilder.buildThresholdBreachMessage).toHaveBeenCalledWith(monitor, check, thresholdBreaches); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 9999, @@ -176,62 +175,6 @@ describe("IncidentService", () => { ); }); - it("uses fallback message when extractThresholdBreaches returns empty array", async () => { - const created = makeIncident(); - const { service, incidentsRepository, notificationMessageBuilder } = createService(); - (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - (notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([]); - - const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), decision, makeCheck()); - - expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ message: "Threshold breach detected" })); - }); - - it("builds the container list for docker threshold_breach incidents", async () => { - const created = makeIncident(); - const { service, incidentsRepository, notificationMessageBuilder } = createService(); - (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - - const monitor = makeMonitor({ type: "docker", status: "breached", dockerAlertOnStopped: true, dockerAlertOnUnhealthy: true }); - 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, decision, check); - - expect(incidentsRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ statusCode: 9999, message: "db: stopped (exit code 1), api: unhealthy" }) - ); - expect(notificationMessageBuilder.extractThresholdBreaches).not.toHaveBeenCalled(); - }); - - it("joins multiple threshold breaches with commas", async () => { - const created = makeIncident(); - const { service, incidentsRepository, notificationMessageBuilder } = createService(); - (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - (incidentsRepository.create as jest.Mock).mockResolvedValue(created); - (notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([ - { metric: "cpu", formattedValue: "95%", threshold: 80, unit: "%" }, - { metric: "memory", formattedValue: "90%", threshold: 80, unit: "%" }, - ]); - - const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), decision, makeCheck()); - - expect(incidentsRepository.create).toHaveBeenCalledWith( - expect.objectContaining({ message: "CPU: 95% (threshold: 80%), MEMORY: 90% (threshold: 80%)" }) - ); - }); - it("resolves active incident when shouldResolveIncident", async () => { const active = makeIncident(); const resolved = makeIncident({ status: false, endTime: "123", resolutionType: "automatic" }); diff --git a/server/test/unit/services/notificationMessageBuilder.test.ts b/server/test/unit/services/notificationMessageBuilder.test.ts index 472542e09..7560b1168 100644 --- a/server/test/unit/services/notificationMessageBuilder.test.ts +++ b/server/test/unit/services/notificationMessageBuilder.test.ts @@ -627,4 +627,51 @@ describe("NotificationMessageBuilder", () => { }); }); }); + + // ── buildThresholdBreachMessage ───────────────────────────────────────── + + describe("buildThresholdBreachMessage", () => { + const none: HardwareBreaches = { cpu: false, memory: false, disk: false, temp: false }; + const hardwareMonitor = () => makeMonitor({ type: "hardware", cpuAlertThreshold: 80, memoryAlertThreshold: 80 }); + + it("describes a single flagged metric", () => { + const check = makeCheck({ cpu: { usage_percent: 0.95, temperature: [50] } }); + + const message = builder.buildThresholdBreachMessage(hardwareMonitor(), check, { ...none, cpu: true }); + + expect(message).toBe("CPU: 95.0% (threshold: 80%)"); + }); + + it("joins several flagged metrics with a comma", () => { + const check = makeCheck({ cpu: { usage_percent: 0.95, temperature: [50] }, memory: { usage_percent: 0.9 } }); + + const message = builder.buildThresholdBreachMessage(hardwareMonitor(), check, { ...none, cpu: true, memory: true }); + + expect(message).toBe("CPU: 95.0% (threshold: 80%), MEMORY: 90.0% (threshold: 80%)"); + }); + + it("falls back when no metric is flagged", () => { + const check = makeCheck({ cpu: { usage_percent: 0.95, temperature: [50] } }); + + expect(builder.buildThresholdBreachMessage(hardwareMonitor(), check, none)).toBe("Threshold breach detected"); + expect(builder.buildThresholdBreachMessage(hardwareMonitor(), check, undefined)).toBe("Threshold breach detected"); + }); + + it("lists the breaching containers for a docker monitor", () => { + const check = makeCheck({ + status: true, + containers: makeContainers([{ name: "db", state: "exited", exitCode: 1 }, { name: "api", health: "unhealthy" }, { name: "web" }]), + }); + + const message = builder.buildThresholdBreachMessage(makeDockerMonitor(), check, undefined); + + expect(message).toBe("db: stopped (exit code 1), api: unhealthy"); + }); + + it("falls back when no container is breaching", () => { + const check = makeCheck({ status: true, containers: makeContainers([{ name: "web" }]) }); + + expect(builder.buildThresholdBreachMessage(makeDockerMonitor(), check, undefined)).toBe("Container alert"); + }); + }); }); diff --git a/server/test/unit/services/notificationsService.test.ts b/server/test/unit/services/notificationsService.test.ts index 7abba9d7f..c621fe0e4 100644 --- a/server/test/unit/services/notificationsService.test.ts +++ b/server/test/unit/services/notificationsService.test.ts @@ -33,7 +33,7 @@ const createSettingsService = (clientHost = "https://app.example.com") => ({ const createMessageBuilder = () => ({ buildMessage: jest.fn().mockReturnValue({ type: "monitor_down", content: { title: "Down" } }), - extractThresholdBreaches: jest.fn(), + buildThresholdBreachMessage: jest.fn(), }); const createService = (overrides?: Record) => {