update tests

This commit is contained in:
Alex Holliday committed 2026-09-24 14:03:34 -07:00
1 parent 02e6333c3c
commit 18f38818e2
5 files changed
+55 -95

No files matched your search

+9 -1
View File
@@ -10,6 +10,8 @@ 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 { Check } from "../../src/domain/checks/check.type.ts";
import type { MonitorActionDecision } from "../../src/worker/worker.interface.ts";
import type { MonitorStatusResponse } from "../../src/types/network.ts";
import type { MaintenanceWindow } from "../../src/domain/maintenance-windows/maintenance-window.type.ts";
import type { EgressStatus } from "../../src/domain/egress/egress.type.ts";
@@ -117,7 +119,13 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness {
nextEgressStatus = status;
};
const notificationReactor = new NotificationReactor(notificationsService as any);
// The real NotificationsService ignores decisions with shouldSendNotification false; the stub is reached only for
// the ones it would act on, so tests can assert on handleNotifications calls as "notifications sent".
const guardedNotificationsService = {
handleNotifications: (monitor: Monitor, check: Check, decision: MonitorActionDecision) =>
decision.shouldSendNotification ? notificationsService.handleNotifications(monitor, check, decision) : Promise.resolve(false),
};
const notificationReactor = new NotificationReactor(guardedNotificationsService as any);
const incidentReactor = new IncidentReactor(incidentService as any);
const reactorDispatcher = new ReactorDispatcher(logger, [notificationReactor, incidentReactor]);
@@ -133,7 +133,7 @@ describe("Evaluate cursor with more than one processing worker", () => {
statusService: {
updateMonitorStatus: async (check: Check, monitor: Monitor) => {
evaluatedCheckIds.push(check.id);
return { monitor, statusChanged: false, prevStatus: monitor.status, code: 200, timestamp: 0 };
return { monitor, statusChanged: false, prevStatus: monitor.status };
},
} as any,
dispatcher: { dispatch: jest.fn<any>().mockResolvedValue(undefined) } as any,
@@ -144,15 +144,6 @@ describe("NotificationMessageBuilder", () => {
expect(msg.severity).toBe("success");
expect(msg.content.title).toBe("Thresholds Resolved: Test Monitor");
});
it("uses notificationReason from decision, falling back to status_change", () => {
const monitor = makeMonitor({ status: "down" });
const decision = makeDecision({ notificationReason: null });
const msg = builder.buildMessage(monitor, makeCheck(), decision, "https://app.example.com");
expect(msg.metadata.notificationReason).toBe("status_change");
});
});
// ── determineNotificationType (via buildMessage) ─────────────────────
+40 -77
View File
@@ -131,92 +131,86 @@ const makeExistingStats = (overrides?: Record<string, unknown>) => ({
// ── Tests ────────────────────────────────────────────────────────────────────
describe("StatusService", () => {
// ── updateRunningStats ───────────────────────────────────────────────────
// ── running stats (via updateMonitorStatus) ──────────────────────────────
describe("updateRunningStats", () => {
// The service now delegates the entire running-stats computation to the repository's
// atomic `updateByMonitorId`, so the service-level tests only verify forwarding and error
// handling. The math (running average, uptimePercentage, timeOfLastFailure state machine)
// is a repository-level concern and is exercised by the repository tests.
describe("running stats", () => {
// The service delegates the running-stats computation to the repository's atomic `updateByMonitorId`,
// so these cases only verify what is forwarded and that a failure never blocks the status update.
// The math (running average, uptimePercentage, timeOfLastFailure) is exercised by the repository tests.
const run = async (check: Check) => {
const monitor = makeMonitor();
const deps = createService();
(deps.monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(deps.monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
const result = await deps.service.updateMonitorStatus(check, monitor);
return { ...deps, result };
};
it("forwards a successful check to updateByMonitorId", async () => {
const { service, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
const { monitorStatsRepository } = await run(makeCheck({ responseTime: 50 }));
const result = await service.updateRunningStats(makeCheck({ responseTime: 50 }), makeMonitor());
expect(result).toBe(true);
expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith(
"mon-1",
expect.objectContaining({
status: true,
responseTime: 50,
now: expect.any(Number),
})
expect.objectContaining({ status: true, responseTime: 50, now: expect.any(Number) })
);
});
it("forwards a failed check to updateByMonitorId", async () => {
const { service, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
const { monitorStatsRepository } = await run(makeCheck({ 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 }));
});
it("passes 0 when responseTime is undefined", async () => {
const { service, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
await service.updateRunningStats(makeCheck({ responseTime: undefined as any }), makeMonitor());
const { monitorStatsRepository } = await run(makeCheck({ responseTime: undefined as any }));
expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ responseTime: 0 }));
});
it("forwards responseTime of 0 as 0 (falsy but defined)", async () => {
const { service, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
await service.updateRunningStats(makeCheck({ responseTime: 0 }), makeMonitor());
const { monitorStatsRepository } = await run(makeCheck({ responseTime: 0 }));
expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ responseTime: 0 }));
});
it("coerces a non-true status to false (e.g. undefined)", async () => {
const { service, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockResolvedValue({});
await service.updateRunningStats(makeCheck({ status: undefined as any }), makeMonitor());
const { monitorStatsRepository } = await run(makeCheck({ status: undefined as any }));
expect(monitorStatsRepository.updateByMonitorId).toHaveBeenCalledWith("mon-1", expect.objectContaining({ status: false }));
});
it("returns false and logs error when updateByMonitorId throws", async () => {
const { service, logger, monitorStatsRepository } = createService();
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue(new Error("db write failed"));
it("warns once and still updates the status when updateByMonitorId throws", async () => {
const monitor = makeMonitor({ statusWindow: [true, true, true, true], statusWindowSize: 5, status: "up" });
const { service, logger, monitorsRepository, monitorStatsRepository } = createService();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue(new Error("stats db down"));
const result = await service.updateRunningStats(makeCheck(), makeMonitor());
const result = await service.updateMonitorStatus(makeCheck({ status: true }), monitor);
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(
expect(result.monitor.status).toBe("up");
expect(monitorsRepository.updateStatusWindowAndChecks).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
service: "StatusService",
method: "updateRunningStats",
message: "db write failed",
method: "tryUpdateRunningStats",
message: "stats db down",
stack: expect.any(String),
})
);
expect(logger.error).not.toHaveBeenCalled();
});
it("logs error with 'Unknown error' for non-Error exceptions", async () => {
const { service, logger, monitorStatsRepository } = createService();
it("warns with 'Unknown error' for non-Error rejections", async () => {
const monitor = makeMonitor();
const { service, logger, monitorsRepository, monitorStatsRepository } = createService();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(monitorStatsRepository.updateByMonitorId as jest.Mock).mockRejectedValue("string error");
const result = await service.updateRunningStats(makeCheck(), makeMonitor());
await service.updateMonitorStatus(makeCheck(), monitor);
expect(result).toBe(false);
expect(logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
});
});
@@ -640,37 +634,6 @@ describe("StatusService", () => {
expect(result.monitor.status).toBe("down");
});
it("logs a warning but still succeeds when running-stats update fails mid-flight", async () => {
const monitor = makeMonitor({ statusWindow: [true, true, true, true], statusWindowSize: 5, status: "up" });
const { service, logger, monitorsRepository, monitorStatsRepository } = createService();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(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(makeCheck({ status: true }), monitor);
expect(result.monitor.status).toBe("up");
expect(logger.warn).toHaveBeenCalledWith(
expect.objectContaining({
service: "StatusService",
method: "updateMonitorStatus",
message: expect.stringContaining("Stats update failed"),
})
);
});
it("returns code and timestamp in result", async () => {
const monitor = makeMonitor({ statusWindow: [true, true, true, true], statusWindowSize: 5 });
const { service, monitorsRepository } = createService();
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
(monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m));
const result = await service.updateMonitorStatus(makeCheck({ statusCode: 201 }), monitor);
expect(result.code).toBe(201);
expect(result.timestamp).toBeGreaterThan(0);
});
// ── Hardware threshold breach tests ──────────────────────────────────
describe("hardware threshold breaches", () => {
@@ -64,8 +64,6 @@ const makeStatusChange = (overrides: Partial<StatusChangeResult> & { status?: st
monitor: { id: "m1", status } as any,
statusChanged: false,
prevStatus: "up",
code: 200,
timestamp: 0,
...rest,
} as StatusChangeResult;
};
@@ -706,7 +704,7 @@ describe("WorkerPipeline", () => {
it("returns an evaluation that opens an incident on a down transition", async () => {
const check = makeCheck({ status: false, statusCode: 500, message: "Error" });
const statusChange = makeStatusChange({ monitor: makeMonitor({ status: "down" }), statusChanged: true, prevStatus: "up", code: 500 });
const statusChange = makeStatusChange({ monitor: makeMonitor({ status: "down" }), statusChanged: true, prevStatus: "up" });
const { pipeline } = createPipeline({
statusService: { updateMonitorStatus: jest.fn<any>().mockResolvedValue(statusChange) },
});
@@ -802,7 +800,7 @@ describe("WorkerPipeline", () => {
});
it("creates incident and notifies when monitor goes down", async () => {
const decision = await decide(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up", code: 500 }));
const decision = await decide(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up" }));
expect(decision).toMatchObject({
shouldCreateIncident: true,
shouldSendNotification: true,
@@ -854,7 +852,7 @@ describe("WorkerPipeline", () => {
it("leaves threshold breaches off a down decision even when the status change carries them", async () => {
const thresholdBreaches = { cpu: true, memory: false, disk: false, temp: false };
const decision = await decide(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up", code: 500, thresholdBreaches }));
const decision = await decide(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up", thresholdBreaches }));
expect(decision).not.toHaveProperty("thresholdBreaches");
});
@@ -876,8 +874,8 @@ describe("WorkerPipeline", () => {
it("starts every decision clean after a transition on the same pipeline", async () => {
const updateMonitorStatus = jest
.fn<any>()
.mockResolvedValueOnce(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up", code: 500 }))
.mockResolvedValueOnce(makeStatusChange({ status: "down", statusChanged: false, prevStatus: "down", code: 500 }));
.mockResolvedValueOnce(makeStatusChange({ status: "down", statusChanged: true, prevStatus: "up" }))
.mockResolvedValueOnce(makeStatusChange({ status: "down", statusChanged: false, prevStatus: "down" }));
const { pipeline } = createPipeline({ statusService: { updateMonitorStatus } });
const failing = makeCheck({ status: false, statusCode: 500, message: "Error" });