diff --git a/server/test/integration/incidentLifecycle.test.ts b/server/test/integration/incidentLifecycle.test.ts index 037713b36..ff2ff2c53 100644 --- a/server/test/integration/incidentLifecycle.test.ts +++ b/server/test/integration/incidentLifecycle.test.ts @@ -7,6 +7,7 @@ import type { IUsersRepository } from "../../src/domain/users/user.repository.in import type { INotificationMessageBuilder } from "../../src/domain/notifications/notification.message-builder.ts"; import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { MonitorActionDecision } from "../../src/worker/worker.interface.ts"; +import type { Check } from "../../src/domain/checks/check.type.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -30,6 +31,17 @@ const makeDecision = (overrides?: Partial): MonitorAction ...overrides, }); +const makeCheck = (statusCode: number): Check => ({ + id: "check-1", + metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, + status: statusCode < 400, + statusCode, + responseTime: 100, + message: statusCode < 400 ? "OK" : "Service Unavailable", + createdAt: "2026-01-01T00:00:00Z", + updatedAt: "2026-01-01T00:00:00Z", +}); + // ── Test suite ─────────────────────────────────────────────────────────────── describe("Incident lifecycle (integration)", () => { @@ -53,7 +65,7 @@ describe("Incident lifecycle (integration)", () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const incident = await service.handleIncident(monitor, 503, decision); + const incident = await service.handleIncident(monitor, decision, makeCheck(503)); expect(incident).not.toBeNull(); expect(incident!.monitorId).toBe("mon-1"); @@ -74,8 +86,8 @@ describe("Incident lifecycle (integration)", () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const first = await service.handleIncident(monitor, 503, decision); - const second = await service.handleIncident(monitor, 503, decision); + const first = await service.handleIncident(monitor, decision, makeCheck(503)); + const second = await service.handleIncident(monitor, decision, makeCheck(503)); expect(second!.id).toBe(first!.id); expect(repo.getAll()).toHaveLength(1); @@ -86,10 +98,10 @@ describe("Incident lifecycle (integration)", () => { it("auto-resolves an incident when monitor recovers", async () => { const monitor = makeMonitor({ status: "down" }); const createDecision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const created = await service.handleIncident(monitor, 503, createDecision); + const created = await service.handleIncident(monitor, createDecision, makeCheck(503)); const resolveDecision = makeDecision({ shouldResolveIncident: true }); - const resolved = await service.handleIncident(monitor, 200, resolveDecision); + const resolved = await service.handleIncident(monitor, resolveDecision, makeCheck(200)); expect(resolved).not.toBeNull(); expect(resolved!.id).toBe(created!.id); @@ -104,7 +116,7 @@ describe("Incident lifecycle (integration)", () => { const monitor = makeMonitor({ status: "up" }); const decision = makeDecision({ shouldResolveIncident: true }); - const result = await service.handleIncident(monitor, 200, decision); + const result = await service.handleIncident(monitor, decision, makeCheck(200)); expect(result).toBeNull(); }); @@ -114,7 +126,7 @@ describe("Incident lifecycle (integration)", () => { it("manually resolves an active incident with comment", async () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const created = await service.handleIncident(monitor, 500, decision); + const created = await service.handleIncident(monitor, decision, makeCheck(500)); const resolved = await service.resolveIncident(created!.id, "user-1", "team-1", "Root cause identified", "user@test.com"); @@ -129,7 +141,7 @@ describe("Incident lifecycle (integration)", () => { it("throws when manually resolving an already-resolved incident", async () => { const monitor = makeMonitor({ status: "down" }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const created = await service.handleIncident(monitor, 500, decision); + const created = await service.handleIncident(monitor, decision, makeCheck(500)); await service.resolveIncident(created!.id, "user-1", "team-1"); @@ -146,7 +158,7 @@ describe("Incident lifecycle (integration)", () => { ]); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - const incident = await service.handleIncident(monitor, 200, decision, { monitorId: "mon-1" } as any); + const incident = await service.handleIncident(monitor, decision, makeCheck(200)); expect(incident!.statusCode).toBe(9999); expect(incident!.message).toBe("CPU: 92% (threshold: 80%), MEMORY: 88% (threshold: 85%)"); @@ -158,7 +170,7 @@ describe("Incident lifecycle (integration)", () => { const monitor = makeMonitor({ status: "up" }); const decision = makeDecision(); - const result = await service.handleIncident(monitor, 200, decision); + const result = await service.handleIncident(monitor, decision, makeCheck(200)); expect(result).toBeNull(); expect(repo.getAll()).toHaveLength(0); @@ -170,19 +182,19 @@ describe("Incident lifecycle (integration)", () => { const monitor = makeMonitor({ status: "down" }); // First outage - const first = await service.handleIncident(monitor, 503, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + const first = await service.handleIncident(monitor, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), makeCheck(503)); expect(first!.status).toBe(true); // Manually resolved await service.resolveIncident(first!.id, "user-1", "team-1", "Restarted server"); // Second outage — new incident since previous was resolved - const second = await service.handleIncident(monitor, 502, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + const second = await service.handleIncident(monitor, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), makeCheck(502)); expect(second!.id).not.toBe(first!.id); expect(second!.statusCode).toBe(502); // Auto-resolve - const resolved = await service.handleIncident(monitor, 200, makeDecision({ shouldResolveIncident: true })); + const resolved = await service.handleIncident(monitor, makeDecision({ shouldResolveIncident: true }), makeCheck(200)); expect(resolved!.id).toBe(second!.id); expect(resolved!.status).toBe(false); expect(resolved!.resolutionType).toBe("automatic"); @@ -200,14 +212,14 @@ describe("Incident lifecycle (integration)", () => { const monitorB = makeMonitor({ id: "mon-b", status: "down" }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); - const incidentA = await service.handleIncident(monitorA, 500, decision); - const incidentB = await service.handleIncident(monitorB, 502, decision); + const incidentA = await service.handleIncident(monitorA, decision, makeCheck(500)); + const incidentB = await service.handleIncident(monitorB, decision, makeCheck(502)); expect(incidentA!.id).not.toBe(incidentB!.id); expect(repo.getAll()).toHaveLength(2); // Resolving monitor A does not affect monitor B - const resolved = await service.handleIncident(monitorA, 200, makeDecision({ shouldResolveIncident: true })); + const resolved = await service.handleIncident(monitorA, makeDecision({ shouldResolveIncident: true }), makeCheck(200)); expect(resolved!.id).toBe(incidentA!.id); const bStillActive = await repo.findActiveByMonitorId("mon-b", "team-1"); diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index f5f978327..44d0a23cf 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.ts @@ -110,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(), makeCheck()); + const result = await service.handleIncident(makeMonitor(), makeDecision(), makeCheck()); expect(result).toBeNull(); }); @@ -121,9 +121,8 @@ describe("IncidentService", () => { const result = await service.handleIncident( makeMonitor(), - 500, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), - makeCheck() + makeCheck({ statusCode: 500 }) ); expect(result).toBe(existing); @@ -138,9 +137,8 @@ describe("IncidentService", () => { const result = await service.handleIncident( makeMonitor(), - 500, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }), - makeCheck() + makeCheck({ statusCode: 500 }) ); expect(result).toBe(created); @@ -166,7 +164,7 @@ describe("IncidentService", () => { 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(monitor, 200, decision, check); + await service.handleIncident(monitor, decision, check); expect(notificationMessageBuilder.extractThresholdBreaches).toHaveBeenCalledWith(monitor, check); expect(incidentsRepository.create).toHaveBeenCalledWith( @@ -185,7 +183,7 @@ describe("IncidentService", () => { (notificationMessageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([]); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, makeCheck()); + await service.handleIncident(makeMonitor(), decision, makeCheck()); expect(incidentsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ message: "Threshold breach detected" })); }); @@ -207,7 +205,7 @@ describe("IncidentService", () => { containerSummary: { total: 3, running: 2, stopped: 1, unhealthy: 1 }, }); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(monitor, 200, decision, check); + await service.handleIncident(monitor, decision, check); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ statusCode: 9999, message: "db: stopped (exit code 1), api: unhealthy" }) @@ -226,7 +224,7 @@ describe("IncidentService", () => { ]); const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); - await service.handleIncident(makeMonitor(), 200, decision, makeCheck()); + await service.handleIncident(makeMonitor(), decision, makeCheck()); expect(incidentsRepository.create).toHaveBeenCalledWith( expect.objectContaining({ message: "CPU: 95% (threshold: 80%), MEMORY: 90% (threshold: 80%)" }) @@ -240,7 +238,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 }), makeCheck()); + const result = await service.handleIncident(makeMonitor(), makeDecision({ shouldResolveIncident: true }), makeCheck()); expect(result).toBe(resolved); expect(incidentsRepository.updateById).toHaveBeenCalledWith( @@ -254,7 +252,7 @@ describe("IncidentService", () => { const { service, incidentsRepository } = createService(); (incidentsRepository.findActiveByMonitorId as jest.Mock).mockResolvedValue(null); - const result = await service.handleIncident(makeMonitor(), 200, makeDecision({ shouldResolveIncident: true }), makeCheck()); + const result = await service.handleIncident(makeMonitor(), makeDecision({ shouldResolveIncident: true }), makeCheck()); expect(result).toBeNull(); });