update tests, add check producer tests
This commit is contained in:
1 parent
e32590425b
commit
5ed9ab780a
7 files changed
+253
-328
No files matched your search
@@ -40,6 +40,11 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository {
|
||||
return this.monitors.filter((m) => monitorIds.includes(m.id)).map((m) => ({ ...m }));
|
||||
}
|
||||
|
||||
async findByIdLean(monitorId: string): Promise<Monitor | null> {
|
||||
const monitor = this.monitors.find((m) => m.id === monitorId);
|
||||
return monitor ? { ...monitor } : null;
|
||||
}
|
||||
|
||||
async findByIdsWithChecks(monitorIds: string[], _checksCount?: number): Promise<Monitor[]> {
|
||||
return this.findByIds(monitorIds);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { WorkerHelper } from "../../src/worker/worker.helper.ts";
|
||||
import { MonitorStatusPolicy } from "../../src/worker/worker.monitor-status-policy.ts";
|
||||
import { CheckPipeline, GeoChecksPipeline } from "../../src/worker/worker.check-pipeline.ts";
|
||||
import { CheckProducer } from "../../src/worker/worker.check-producer.ts";
|
||||
import { CheckEvaluator } from "../../src/worker/worker.check-evaluator.ts";
|
||||
import { NotificationReactor } from "../../src/worker/reactors/reactor.notification.ts";
|
||||
@@ -71,20 +69,6 @@ const createStubMonitorStatsRepo = () => ({
|
||||
deleteByMonitorIdsNotIn: jest.fn(),
|
||||
});
|
||||
|
||||
const createStubChecksRepo = () => ({
|
||||
create: jest.fn(),
|
||||
createChecks: jest.fn(),
|
||||
findByMonitorId: jest.fn(),
|
||||
findByTeamId: jest.fn(),
|
||||
findLatestByMonitorIds: jest.fn(),
|
||||
findByDateRangeAndMonitorId: jest.fn(),
|
||||
findSummaryByTeamId: jest.fn(),
|
||||
deleteByMonitorId: jest.fn(),
|
||||
deleteByTeamId: jest.fn(),
|
||||
deleteByMonitorIdsNotIn: jest.fn(),
|
||||
deleteOlderThan: jest.fn(),
|
||||
});
|
||||
|
||||
export interface HeartbeatTestHarness {
|
||||
monitorsRepo: InMemoryMonitorsRepository;
|
||||
incidentsRepo: InMemoryIncidentsRepository;
|
||||
@@ -108,13 +92,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness {
|
||||
const logger = createMockLogger() as any;
|
||||
const bufferStub = { addToBuffer: jest.fn(), addGeoCheckToBuffer: jest.fn(), scheduleNextFlush: jest.fn() };
|
||||
|
||||
const statusService = new StatusService(
|
||||
logger,
|
||||
bufferStub as any,
|
||||
monitorsRepo as any,
|
||||
createStubMonitorStatsRepo() as any,
|
||||
createStubChecksRepo() as any
|
||||
);
|
||||
const statusService = new StatusService(logger, monitorsRepo as any, createStubMonitorStatsRepo() as any);
|
||||
|
||||
const messageBuilder = { extractThresholdBreaches: jest.fn().mockReturnValue([]) };
|
||||
const incidentService = new IncidentService(logger, incidentsRepo, monitorsRepo as any, { findById: jest.fn() } as any, messageBuilder as any);
|
||||
@@ -149,7 +127,6 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness {
|
||||
};
|
||||
|
||||
const maintenanceWindowsRepo = { findByMonitorId: jest.fn().mockResolvedValue([]) };
|
||||
const geoChecksService = { buildGeoCheck: jest.fn() };
|
||||
|
||||
const checkProducer = new CheckProducer(
|
||||
monitorsRepo as any,
|
||||
@@ -160,28 +137,21 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness {
|
||||
logger
|
||||
);
|
||||
const checkEvaluator = new CheckEvaluator(statusService as any, new MonitorStatusPolicy());
|
||||
const checkPipeline = new CheckPipeline(checkProducer, checkEvaluator);
|
||||
const geoCheckPipeline = new GeoChecksPipeline(maintenanceWindowsRepo as any, geoChecksService as any, bufferStub as any, logger);
|
||||
|
||||
const notificationReactor = new NotificationReactor(notificationsService as any);
|
||||
const incidentReactor = new IncidentReactor(incidentService as any);
|
||||
const reactorDispatcher = new ReactorDispatcher(logger, [notificationReactor, incidentReactor]);
|
||||
|
||||
const helper = new WorkerHelper(
|
||||
logger,
|
||||
checkService as any,
|
||||
{ getDBSettings: jest.fn() } as any,
|
||||
monitorsRepo as any,
|
||||
{ deleteByMonitorIdsNotIn: jest.fn() } as any,
|
||||
{ findAllTeamIds: jest.fn() } as any,
|
||||
createStubMonitorStatsRepo() as any,
|
||||
createStubChecksRepo() as any,
|
||||
incidentsRepo as any,
|
||||
{ deleteByMonitorIdsNotIn: jest.fn() } as any,
|
||||
reactorDispatcher,
|
||||
checkPipeline,
|
||||
geoCheckPipeline
|
||||
);
|
||||
// Mirror the production check→evaluate flow: read the monitor fresh each cycle (so the
|
||||
// accumulated statusWindow is visible), produce a check, then evaluate against that monitor.
|
||||
const heartbeatJob = async (seedMonitor: Monitor) => {
|
||||
const monitor = await monitorsRepo.findByIdLean(seedMonitor.id);
|
||||
if (!monitor) return;
|
||||
const result = await checkProducer.produce(monitor);
|
||||
if (!result) return; // skipped (e.g. maintenance window)
|
||||
const evaluation = await checkEvaluator.evaluate(result.status, result.check, monitor);
|
||||
await reactorDispatcher.dispatch(evaluation);
|
||||
};
|
||||
|
||||
return {
|
||||
monitorsRepo,
|
||||
@@ -193,7 +163,7 @@ export function createHeartbeatTestHarness(): HeartbeatTestHarness {
|
||||
bufferStub,
|
||||
maintenanceWindowsRepo,
|
||||
messageBuilder,
|
||||
heartbeatJob: helper.getHeartbeatJob(),
|
||||
heartbeatJob,
|
||||
setNextResponse,
|
||||
setNextResponseFull,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { CheckPipeline, GeoChecksPipeline } from "../../../src/worker/worker.check-pipeline.ts";
|
||||
import { CheckProducer } from "../../../src/worker/worker.check-producer.ts";
|
||||
import { CheckEvaluator } from "../../../src/worker/worker.check-evaluator.ts";
|
||||
import { MonitorStatusPolicy } from "../../../src/worker/worker.monitor-status-policy.ts";
|
||||
import { GeoChecksPipeline } from "../../../src/worker/worker.check-pipeline.ts";
|
||||
import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
|
||||
@@ -23,114 +20,6 @@ const activeWindow = () => {
|
||||
return { active: true, start: new Date(now - 1000).toISOString(), end: new Date(now + 1000).toISOString(), repeat: 0 };
|
||||
};
|
||||
|
||||
// ── CheckPipeline ────────────────────────────────────────────────────────────
|
||||
|
||||
const createCheckPipeline = (overrides?: Record<string, any>) => {
|
||||
const defaults = {
|
||||
logger: createMockLogger(),
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) },
|
||||
statusService: {
|
||||
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1", status: "up" }, statusChanged: false, prevStatus: "up", code: 200 }),
|
||||
},
|
||||
checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) },
|
||||
buffer: { addToBuffer: jest.fn() },
|
||||
monitorsRepository: { updateById: jest.fn().mockResolvedValue({}) },
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) },
|
||||
monitorStatusPolicy: new MonitorStatusPolicy(),
|
||||
...overrides,
|
||||
};
|
||||
const checkProducer = new CheckProducer(
|
||||
defaults.monitorsRepository as any,
|
||||
defaults.maintenanceWindowsRepository as any,
|
||||
defaults.checkService as any,
|
||||
defaults.networkService as any,
|
||||
defaults.buffer as any,
|
||||
defaults.logger as any
|
||||
);
|
||||
const checkEvaluator = new CheckEvaluator(defaults.statusService as any, defaults.monitorStatusPolicy as any);
|
||||
const pipeline = new CheckPipeline(checkProducer, checkEvaluator);
|
||||
return { pipeline, defaults };
|
||||
};
|
||||
|
||||
describe("CheckPipeline", () => {
|
||||
it("throws when monitor id is missing", async () => {
|
||||
const { pipeline } = createCheckPipeline();
|
||||
await expect(pipeline.run({} as Monitor)).rejects.toThrow("No monitor id");
|
||||
});
|
||||
|
||||
it("skips and sets maintenance status when in maintenance window", async () => {
|
||||
const { pipeline, defaults } = createCheckPipeline({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) },
|
||||
});
|
||||
|
||||
const result = await pipeline.run(makeMonitor({ status: "up" }));
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(defaults.networkService.requestStatus).not.toHaveBeenCalled();
|
||||
expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance" });
|
||||
});
|
||||
|
||||
it("does not re-write status when already in maintenance", async () => {
|
||||
const { pipeline, defaults } = createCheckPipeline({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) },
|
||||
});
|
||||
|
||||
await pipeline.run(makeMonitor({ status: "maintenance" }));
|
||||
|
||||
expect(defaults.monitorsRepository.updateById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("throws when network response is null", async () => {
|
||||
const { pipeline } = createCheckPipeline({
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue(null) },
|
||||
});
|
||||
await expect(pipeline.run(makeMonitor())).rejects.toThrow("No network response");
|
||||
});
|
||||
|
||||
it("returns null and warns when toCheck returns null", async () => {
|
||||
const { pipeline, defaults } = createCheckPipeline({
|
||||
checkService: { toCheck: jest.fn().mockReturnValue(null) },
|
||||
});
|
||||
|
||||
const result = await pipeline.run(makeMonitor());
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("No check could be built") }));
|
||||
expect(defaults.statusService.updateMonitorStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("buffers the check, updates status, and returns an evaluation", async () => {
|
||||
const status = { monitorId: "m1", status: false, code: 500, message: "Error" };
|
||||
const statusChange = { monitor: { id: "m1", status: "down" }, statusChanged: true, prevStatus: "up", code: 500 };
|
||||
const { pipeline, defaults } = createCheckPipeline({
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue(status) },
|
||||
statusService: { updateMonitorStatus: jest.fn().mockResolvedValue(statusChange) },
|
||||
});
|
||||
|
||||
const result = await pipeline.run(makeMonitor());
|
||||
|
||||
expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith({ id: "check-1" });
|
||||
expect(defaults.statusService.updateMonitorStatus).toHaveBeenCalled();
|
||||
expect(result).toMatchObject({
|
||||
monitor: statusChange.monitor,
|
||||
status,
|
||||
check: { id: "check-1" },
|
||||
statusChange,
|
||||
decision: expect.objectContaining({ shouldCreateIncident: true, shouldSendNotification: true, incidentReason: "status_down" }),
|
||||
});
|
||||
});
|
||||
|
||||
it("proceeds with the check when no maintenance window is active", async () => {
|
||||
const { pipeline, defaults } = createCheckPipeline({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([{ ...activeWindow(), active: false }]) },
|
||||
});
|
||||
|
||||
await pipeline.run(makeMonitor());
|
||||
|
||||
expect(defaults.networkService.requestStatus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── GeoChecksPipeline ──────────────────────────────────────────────────────────
|
||||
|
||||
const createGeoPipeline = (overrides?: Record<string, any>) => {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { CheckProducer } from "../../../src/worker/worker.check-producer.ts";
|
||||
import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
|
||||
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
({
|
||||
id: "m1",
|
||||
teamId: "team",
|
||||
type: "http",
|
||||
interval: 60000,
|
||||
status: "up",
|
||||
...overrides,
|
||||
}) as Monitor;
|
||||
|
||||
// A maintenance window straddling "now" — active per isWindowActive.
|
||||
const activeWindow = () => {
|
||||
const now = Date.now();
|
||||
return { active: true, start: new Date(now - 1000).toISOString(), end: new Date(now + 1000).toISOString(), repeat: 0 };
|
||||
};
|
||||
|
||||
const createProducer = (overrides?: Record<string, any>) => {
|
||||
const defaults = {
|
||||
logger: createMockLogger(),
|
||||
monitorsRepository: { updateById: jest.fn().mockResolvedValue({}) },
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([]) },
|
||||
checkService: { toCheck: jest.fn().mockReturnValue({ id: "check-1" }) },
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue({ monitorId: "m1", status: true, code: 200, message: "OK" }) },
|
||||
buffer: { addToBuffer: jest.fn() },
|
||||
...overrides,
|
||||
};
|
||||
const producer = new CheckProducer(
|
||||
defaults.monitorsRepository as any,
|
||||
defaults.maintenanceWindowsRepository as any,
|
||||
defaults.checkService as any,
|
||||
defaults.networkService as any,
|
||||
defaults.buffer as any,
|
||||
defaults.logger as any
|
||||
);
|
||||
return { producer, defaults };
|
||||
};
|
||||
|
||||
describe("CheckProducer", () => {
|
||||
it("throws when monitor id is missing", async () => {
|
||||
const { producer } = createProducer();
|
||||
await expect(producer.produce({} as Monitor)).rejects.toThrow("No monitor id");
|
||||
});
|
||||
|
||||
// ── maintenance gate ──────────────────────────────────────────────────────
|
||||
|
||||
it("skips the check and flips status to 'maintenance' when in a maintenance window", async () => {
|
||||
const { producer, defaults } = createProducer({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) },
|
||||
});
|
||||
|
||||
const result = await producer.produce(makeMonitor({ status: "up" }));
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(defaults.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { status: "maintenance" });
|
||||
expect(defaults.networkService.requestStatus).not.toHaveBeenCalled();
|
||||
expect(defaults.buffer.addToBuffer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not re-write status when already in maintenance", async () => {
|
||||
const { producer, defaults } = createProducer({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([activeWindow()]) },
|
||||
});
|
||||
|
||||
const result = await producer.produce(makeMonitor({ status: "maintenance" }));
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(defaults.monitorsRepository.updateById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("proceeds with the check when the maintenance window is inactive", async () => {
|
||||
const { producer, defaults } = createProducer({
|
||||
maintenanceWindowsRepository: { findByMonitorId: jest.fn().mockResolvedValue([{ ...activeWindow(), active: false }]) },
|
||||
});
|
||||
|
||||
await producer.produce(makeMonitor());
|
||||
|
||||
expect(defaults.networkService.requestStatus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// ── acquire / record ──────────────────────────────────────────────────────
|
||||
|
||||
it("throws when the network response is null", async () => {
|
||||
const { producer } = createProducer({
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue(null) },
|
||||
});
|
||||
|
||||
await expect(producer.produce(makeMonitor())).rejects.toThrow("No network response");
|
||||
});
|
||||
|
||||
it("returns null, warns, and does not buffer when toCheck yields nothing", async () => {
|
||||
const { producer, defaults } = createProducer({
|
||||
checkService: { toCheck: jest.fn().mockReturnValue(null) },
|
||||
});
|
||||
|
||||
const result = await producer.produce(makeMonitor());
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("No check could be built") }));
|
||||
expect(defaults.buffer.addToBuffer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("buffers the built check and returns the status and check on success", async () => {
|
||||
const status = { monitorId: "m1", status: false, code: 500, message: "Error" };
|
||||
const check = { id: "check-1" };
|
||||
const { producer, defaults } = createProducer({
|
||||
networkService: { requestStatus: jest.fn().mockResolvedValue(status) },
|
||||
checkService: { toCheck: jest.fn().mockReturnValue(check) },
|
||||
});
|
||||
|
||||
const result = await producer.produce(makeMonitor());
|
||||
|
||||
expect(defaults.buffer.addToBuffer).toHaveBeenCalledWith(check);
|
||||
expect(result).toEqual({ status, check });
|
||||
});
|
||||
});
|
||||
@@ -80,6 +80,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses
|
||||
};
|
||||
const monitorsRepository = {
|
||||
findByIds: jest.fn<any>().mockResolvedValue([makeMonitor()]),
|
||||
findByIdLean: jest.fn<any>().mockResolvedValue(makeMonitor()),
|
||||
findAll: jest.fn<any>().mockResolvedValue([]),
|
||||
updateById: jest.fn<any>().mockResolvedValue({}),
|
||||
};
|
||||
@@ -94,7 +95,7 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses
|
||||
produce: jest.fn<any>().mockResolvedValue({ status: { status: "up" }, check: { id: "c1" } }),
|
||||
};
|
||||
const checkEvaluator = {
|
||||
evaluate: jest.fn<any>().mockResolvedValue({ monitor: makeMonitor(), decision: {} }),
|
||||
evaluate: jest.fn<any>().mockResolvedValue({ monitor: makeMonitor(), statusChange: { monitor: makeMonitor() }, decision: {} }),
|
||||
};
|
||||
const geoCheckPipeline = { run: jest.fn<any>().mockResolvedValue(null) };
|
||||
const dispatcher = { dispatch: jest.fn<any>().mockResolvedValue(undefined) };
|
||||
@@ -268,7 +269,7 @@ describe("DBQueueWorker", () => {
|
||||
const job = makeJob({ type: "check", refId: "m1", intervalMs: 60000 });
|
||||
const { mocks } = await start({ mocks: { jobsRepository: { ...createWorker().mocks.jobsRepository, claimDueBatch: claimOnce(job) } } });
|
||||
|
||||
expect(mocks.monitorsRepository.findByIds).toHaveBeenCalledWith(["m1"]);
|
||||
expect(mocks.monitorsRepository.findByIdLean).toHaveBeenCalledWith("m1");
|
||||
expect(mocks.checkProducer.produce).toHaveBeenCalled();
|
||||
// The check stage only produces; the buffer arms evaluate once the check is durably stored
|
||||
expect(mocks.jobsRepository.upsertEvaluate).not.toHaveBeenCalled();
|
||||
@@ -307,6 +308,35 @@ describe("DBQueueWorker", () => {
|
||||
expect(mocks.monitorsRepository.updateById).toHaveBeenCalledWith("m1", "team", { lastEvaluatedAt: 12345 });
|
||||
});
|
||||
|
||||
it("evaluate job reads the monitor once and threads the post-write monitor across the backlog", async () => {
|
||||
const job = makeJob({ id: "evaluate:m1", type: "evaluate", intervalMs: null });
|
||||
const initialMonitor = makeMonitor();
|
||||
const postWriteMonitor = makeMonitor({ status: "down" }); // what updateStatusWindowAndChecks would return after check c1
|
||||
const monitorsRepository = {
|
||||
findByIdLean: jest.fn<any>().mockResolvedValue(initialMonitor),
|
||||
updateById: jest.fn<any>().mockResolvedValue({}),
|
||||
};
|
||||
const checksRepository = { findUnevaluatedByMonitorId: jest.fn<any>().mockResolvedValue([{ id: "c1" }, { id: "c2" }]) };
|
||||
const checkEvaluator = {
|
||||
evaluate: jest.fn<any>().mockResolvedValue({ monitor: postWriteMonitor, statusChange: { monitor: postWriteMonitor }, decision: {} }),
|
||||
};
|
||||
const { mocks } = await start({
|
||||
mocks: {
|
||||
jobsRepository: { ...createWorker().mocks.jobsRepository, claimDueBatch: claimOnce(job) },
|
||||
monitorsRepository,
|
||||
checksRepository,
|
||||
checkEvaluator,
|
||||
},
|
||||
});
|
||||
|
||||
// The monitor is read once for the whole backlog, not once per check
|
||||
expect(mocks.monitorsRepository.findByIdLean).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.checkEvaluator.evaluate).toHaveBeenCalledTimes(2);
|
||||
// First check evaluates against the freshly-read monitor; the second against the post-write monitor from the first
|
||||
expect(mocks.checkEvaluator.evaluate.mock.calls[0][2]).toBe(initialMonitor);
|
||||
expect(mocks.checkEvaluator.evaluate.mock.calls[1][2]).toBe(postWriteMonitor);
|
||||
});
|
||||
|
||||
it("cleanup-orphaned job runs the helper's cleanup function", async () => {
|
||||
const job = makeJob({ id: "cleanup-orphaned", type: "cleanup-orphaned", refId: null, intervalMs: 86400000 });
|
||||
const cleanupFn = jest.fn<any>().mockResolvedValue(undefined);
|
||||
|
||||
@@ -7,21 +7,15 @@ import type { Check } from "../../../src/domain/checks/check.type.ts";
|
||||
import type { MonitorStatusResponse, HardwareStatusPayload } from "../../../src/types/network.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";
|
||||
import type { IChecksRepository } from "../../../src/domain/checks/check.repository.interface.ts";
|
||||
import type { IBufferService } from "../../../src/service/bufferService.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const createBuffer = (): jest.Mocked<Pick<IBufferService, "addToBuffer">> => ({
|
||||
addToBuffer: jest.fn(),
|
||||
});
|
||||
|
||||
const createMonitorsRepo = () => {
|
||||
const findById = jest.fn();
|
||||
const updateStatusWindowAndChecks = jest
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
(
|
||||
async (
|
||||
_id: unknown,
|
||||
_tid: unknown,
|
||||
status: boolean,
|
||||
@@ -30,27 +24,23 @@ const createMonitorsRepo = () => {
|
||||
maxRecentChecks: number,
|
||||
statusPatch?: Partial<Monitor>
|
||||
) => {
|
||||
// Grab the monitor that findById was configured to resolve with
|
||||
const monitor = findById.mock.results.at(-1)?.value;
|
||||
if (monitor && typeof monitor.then === "function") {
|
||||
return monitor.then((m: any) => {
|
||||
m.statusWindow = m.statusWindow || [];
|
||||
m.statusWindow.push(status);
|
||||
while (m.statusWindow.length > windowSize) {
|
||||
m.statusWindow.shift();
|
||||
}
|
||||
m.recentChecks = m.recentChecks || [];
|
||||
m.recentChecks.push(checkSnapshot);
|
||||
while (m.recentChecks.length > maxRecentChecks) {
|
||||
m.recentChecks.shift();
|
||||
}
|
||||
if (statusPatch) {
|
||||
Object.assign(m, statusPatch);
|
||||
}
|
||||
return { ...m };
|
||||
});
|
||||
// updateMonitorStatus no longer reads the monitor itself — tests seed it via
|
||||
// findById.mockResolvedValue, so the mock resolves it here as the post-write monitor.
|
||||
const m: any = await findById();
|
||||
m.statusWindow = m.statusWindow || [];
|
||||
m.statusWindow.push(status);
|
||||
while (m.statusWindow.length > windowSize) {
|
||||
m.statusWindow.shift();
|
||||
}
|
||||
return Promise.reject(new Error("findById not mocked"));
|
||||
m.recentChecks = m.recentChecks || [];
|
||||
m.recentChecks.push(checkSnapshot);
|
||||
while (m.recentChecks.length > maxRecentChecks) {
|
||||
m.recentChecks.shift();
|
||||
}
|
||||
if (statusPatch) {
|
||||
Object.assign(m, statusPatch);
|
||||
}
|
||||
return { ...m };
|
||||
}
|
||||
);
|
||||
return {
|
||||
@@ -67,23 +57,17 @@ const createMonitorStatsRepo = () =>
|
||||
updateByMonitorId: jest.fn(),
|
||||
}) as unknown as jest.Mocked<IMonitorStatsRepository>;
|
||||
|
||||
const createChecksRepo = () => ({}) as unknown as jest.Mocked<IChecksRepository>;
|
||||
|
||||
const createService = (overrides?: {
|
||||
logger?: ReturnType<typeof createMockLogger>;
|
||||
buffer?: ReturnType<typeof createBuffer>;
|
||||
monitorsRepository?: ReturnType<typeof createMonitorsRepo>;
|
||||
monitorStatsRepository?: ReturnType<typeof createMonitorStatsRepo>;
|
||||
checksRepository?: ReturnType<typeof createChecksRepo>;
|
||||
}) => {
|
||||
const logger = overrides?.logger ?? createMockLogger();
|
||||
const buffer = overrides?.buffer ?? createBuffer();
|
||||
const monitorsRepository = overrides?.monitorsRepository ?? createMonitorsRepo();
|
||||
const monitorStatsRepository = overrides?.monitorStatsRepository ?? createMonitorStatsRepo();
|
||||
const checksRepository = overrides?.checksRepository ?? createChecksRepo();
|
||||
|
||||
const service = new StatusService(logger as any, buffer as any, monitorsRepository, monitorStatsRepository, checksRepository);
|
||||
return { service, logger, buffer, monitorsRepository, monitorStatsRepository, checksRepository };
|
||||
const service = new StatusService(logger as any, monitorsRepository, monitorStatsRepository);
|
||||
return { service, logger, monitorsRepository, monitorStatsRepository };
|
||||
};
|
||||
|
||||
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
@@ -264,19 +248,31 @@ 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());
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.prevStatus).toBe("up");
|
||||
});
|
||||
|
||||
it("uses the prefetched monitor and skips findById when one is passed", async () => {
|
||||
const monitor = makeMonitor({ statusWindow: [], statusWindowSize: 5, status: "up" });
|
||||
const { service, monitorsRepository } = createService();
|
||||
(monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockResolvedValue(monitor);
|
||||
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor);
|
||||
|
||||
expect(monitorsRepository.findById).not.toHaveBeenCalled();
|
||||
expect(result.prevStatus).toBe("up");
|
||||
expect(result.monitor).toBe(monitor);
|
||||
});
|
||||
|
||||
it("pushes to statusWindow and trims to statusWindowSize", async () => {
|
||||
const monitor = makeMonitor({ statusWindow: [true, 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));
|
||||
|
||||
await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }));
|
||||
await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
// Atomic push should have been called with the correct status and window size
|
||||
expect(monitorsRepository.updateStatusWindowAndChecks).toHaveBeenCalledWith(
|
||||
@@ -297,7 +293,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" }));
|
||||
await service.updateMonitorStatus(makeStatusResponse(), 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" }));
|
||||
@@ -315,7 +311,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -333,7 +329,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("up");
|
||||
@@ -350,7 +346,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());
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("up");
|
||||
@@ -370,7 +366,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("up");
|
||||
@@ -387,7 +383,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -399,7 +395,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());
|
||||
await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor);
|
||||
|
||||
expect(monitor.statusWindow).toEqual([true]);
|
||||
});
|
||||
@@ -410,23 +406,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));
|
||||
|
||||
await service.updateMonitorStatus(makeStatusResponse(), makeCheck());
|
||||
await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor);
|
||||
|
||||
expect(monitor.recentChecks).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("throws AppError when repository throws", async () => {
|
||||
const monitor = makeMonitor();
|
||||
const { service, monitorsRepository } = createService();
|
||||
(monitorsRepository.findById as jest.Mock).mockRejectedValue(new Error("db error"));
|
||||
(monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue(new Error("db error"));
|
||||
|
||||
await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck())).rejects.toThrow("Failed to update monitor");
|
||||
await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor)).rejects.toThrow("Failed to update monitor");
|
||||
});
|
||||
|
||||
it("throws AppError with 'Unknown error' for non-Error exceptions", async () => {
|
||||
const monitor = makeMonitor();
|
||||
const { service, monitorsRepository } = createService();
|
||||
(monitorsRepository.findById as jest.Mock).mockRejectedValue("string error");
|
||||
(monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mockRejectedValue("string error");
|
||||
|
||||
await expect(service.updateMonitorStatus(makeStatusResponse(), makeCheck())).rejects.toThrow("Unknown error");
|
||||
await expect(service.updateMonitorStatus(makeStatusResponse(), 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 () => {
|
||||
@@ -444,7 +442,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -462,7 +460,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -484,7 +482,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor);
|
||||
|
||||
expect(result.monitor.status).toBe("up");
|
||||
});
|
||||
@@ -502,7 +500,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.monitor.status).toBe("down");
|
||||
});
|
||||
@@ -518,7 +516,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("up");
|
||||
@@ -535,7 +533,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -550,7 +548,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -567,7 +565,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 }));
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(false);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -580,7 +578,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());
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck(), monitor);
|
||||
|
||||
expect(result.monitor.status).toBe("up");
|
||||
expect(logger.warn).toHaveBeenCalledWith(
|
||||
@@ -598,7 +596,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());
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse({ code: 201 }), makeCheck(), monitor);
|
||||
|
||||
expect(result.code).toBe(201);
|
||||
expect(result.timestamp).toBeGreaterThan(0);
|
||||
@@ -639,7 +637,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.cpu).toBe(true);
|
||||
expect(result.thresholdBreaches?.memory).toBe(false);
|
||||
@@ -656,7 +654,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.memory).toBe(true);
|
||||
const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6];
|
||||
@@ -673,7 +671,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.disk).toBe(true);
|
||||
const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6];
|
||||
@@ -690,7 +688,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.temp).toBe(true);
|
||||
const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6];
|
||||
@@ -712,7 +710,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6];
|
||||
expect(patch.cpuAlertCounter).toBe(5);
|
||||
@@ -731,7 +729,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.monitor.status).toBe("breached");
|
||||
expect(result.statusChanged).toBe(false);
|
||||
@@ -746,7 +744,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("up");
|
||||
@@ -769,7 +767,7 @@ describe("StatusService", () => {
|
||||
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 }));
|
||||
const result = await service.updateMonitorStatus(response, makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.monitor.status).toBe("down");
|
||||
});
|
||||
@@ -781,7 +779,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.cpu).toBe(false);
|
||||
});
|
||||
@@ -793,7 +791,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.memory).toBe(false);
|
||||
});
|
||||
@@ -807,7 +805,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.temp).toBe(false);
|
||||
});
|
||||
@@ -821,7 +819,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.temp).toBe(false);
|
||||
});
|
||||
@@ -833,7 +831,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches).toBeUndefined();
|
||||
});
|
||||
@@ -845,7 +843,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches).toBeUndefined();
|
||||
});
|
||||
@@ -860,7 +858,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());
|
||||
const result = await service.updateMonitorStatus(makeStatusResponse(), makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches).toBeUndefined();
|
||||
});
|
||||
@@ -874,7 +872,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.disk).toBe(false);
|
||||
});
|
||||
@@ -888,7 +886,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.disk).toBe(false);
|
||||
});
|
||||
@@ -902,7 +900,7 @@ describe("StatusService", () => {
|
||||
const response = makeHardwareResponse({
|
||||
data: { cpu: undefined, memory: { usage_percent: 0.1 }, disk: [], host: {} },
|
||||
} as any);
|
||||
const result = await service.updateMonitorStatus(response, makeCheck());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.cpu).toBe(false);
|
||||
expect(result.thresholdBreaches?.temp).toBe(false);
|
||||
@@ -917,7 +915,7 @@ describe("StatusService", () => {
|
||||
const response = makeHardwareResponse({
|
||||
data: { cpu: { usage_percent: 0.1 }, memory: undefined, disk: [], host: {} },
|
||||
} as any);
|
||||
const result = await service.updateMonitorStatus(response, makeCheck());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.memory).toBe(false);
|
||||
});
|
||||
@@ -931,7 +929,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(result.thresholdBreaches?.disk).toBe(false);
|
||||
});
|
||||
@@ -945,7 +943,7 @@ describe("StatusService", () => {
|
||||
(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());
|
||||
const result = await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
const patch = (monitorsRepository.updateStatusWindowAndChecks as jest.Mock).mock.calls.at(-1)?.[6];
|
||||
expect(patch.cpuAlertCounter).toBe(2);
|
||||
@@ -971,7 +969,7 @@ describe("StatusService", () => {
|
||||
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 }));
|
||||
const result = await service.updateMonitorStatus(response, makeCheck({ status: false }), monitor);
|
||||
|
||||
expect(result.statusChanged).toBe(true);
|
||||
expect(result.monitor.status).toBe("down");
|
||||
@@ -991,7 +989,7 @@ describe("StatusService", () => {
|
||||
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());
|
||||
await service.updateMonitorStatus(response, makeCheck(), monitor);
|
||||
|
||||
expect(monitor.cpuAlertCounter).toBe(0);
|
||||
expect(monitor.memoryAlertCounter).toBe(0);
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { describe, expect, it, jest } from "@jest/globals";
|
||||
import { WorkerHelper } from "../../../src/worker/worker.helper.ts";
|
||||
import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts";
|
||||
import { createMockLogger } from "../../helpers/createMockLogger.ts";
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// WorkerHelper is now thin glue: it wires the scheduler's job functions to the
|
||||
// CheckPipeline / GeoChecksPipeline (acquire→record→decide) and the
|
||||
// ReactorDispatcher (react), and owns the cleanup jobs. The pipeline and
|
||||
// dispatcher internals are tested in checkPipeline.test.ts / reactorDispatcher.test.ts;
|
||||
// WorkerHelper is now thin glue: it wires the geo job to the GeoChecksPipeline and
|
||||
// owns the cleanup jobs. The pipeline internals are tested in checkPipeline.test.ts;
|
||||
// here we test only the glue + cleanup jobs.
|
||||
|
||||
const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
@@ -42,10 +39,6 @@ const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
deleteOlderThan: jest.fn().mockResolvedValue(0),
|
||||
};
|
||||
|
||||
const checkPipeline = { run: jest.fn().mockResolvedValue(null) };
|
||||
const geoCheckPipeline = { run: jest.fn().mockResolvedValue(null) };
|
||||
const reactorDispatcher = { dispatch: jest.fn().mockResolvedValue(undefined) };
|
||||
|
||||
const defaults = {
|
||||
logger: createMockLogger(),
|
||||
checkService,
|
||||
@@ -57,9 +50,6 @@ const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
checksRepository,
|
||||
incidentsRepository,
|
||||
geoChecksRepository,
|
||||
reactorDispatcher,
|
||||
checkPipeline,
|
||||
geoCheckPipeline,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -73,26 +63,11 @@ const createHelper = (overrides?: Record<string, unknown>) => {
|
||||
defaults.monitorStatsRepository as any,
|
||||
defaults.checksRepository as any,
|
||||
defaults.incidentsRepository as any,
|
||||
defaults.geoChecksRepository as any,
|
||||
defaults.reactorDispatcher as any,
|
||||
defaults.checkPipeline as any,
|
||||
defaults.geoCheckPipeline as any
|
||||
defaults.geoChecksRepository as any
|
||||
);
|
||||
return { helper, defaults };
|
||||
};
|
||||
|
||||
const makeMonitor = (overrides?: Partial<Monitor>): Monitor =>
|
||||
({
|
||||
id: "m1",
|
||||
teamId: "team",
|
||||
type: "http",
|
||||
interval: 60000,
|
||||
status: "up",
|
||||
geoCheckEnabled: false,
|
||||
geoCheckLocations: [],
|
||||
...overrides,
|
||||
}) as Monitor;
|
||||
|
||||
// ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("WorkerHelper", () => {
|
||||
@@ -103,68 +78,6 @@ describe("WorkerHelper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── getHeartbeatJob (glue) ────────────────────────────────────────────────
|
||||
|
||||
describe("getHeartbeatJob", () => {
|
||||
it("runs the pipeline and dispatches the evaluation to reactors", async () => {
|
||||
const evaluation = { monitor: { id: "m1" }, decision: {} };
|
||||
const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockResolvedValue(evaluation) } });
|
||||
|
||||
await helper.getHeartbeatJob()(makeMonitor());
|
||||
|
||||
expect(defaults.checkPipeline.run).toHaveBeenCalled();
|
||||
expect(defaults.reactorDispatcher.dispatch).toHaveBeenCalledWith(evaluation);
|
||||
});
|
||||
|
||||
it("does not dispatch when the pipeline returns null (skipped)", async () => {
|
||||
const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockResolvedValue(null) } });
|
||||
|
||||
await helper.getHeartbeatJob()(makeMonitor());
|
||||
|
||||
expect(defaults.reactorDispatcher.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs and rethrows when the pipeline throws", async () => {
|
||||
const { helper, defaults } = createHelper({ checkPipeline: { run: jest.fn().mockRejectedValue(new Error("boom")) } });
|
||||
|
||||
await expect(helper.getHeartbeatJob()(makeMonitor())).rejects.toThrow("boom");
|
||||
expect(defaults.logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "boom" }));
|
||||
});
|
||||
|
||||
it("rethrows non-Error failures", async () => {
|
||||
const { helper } = createHelper({ checkPipeline: { run: jest.fn().mockRejectedValue("unexpected") } });
|
||||
|
||||
await expect(helper.getHeartbeatJob()(makeMonitor())).rejects.toBe("unexpected");
|
||||
});
|
||||
});
|
||||
|
||||
// ── getHeartbeatGeoJob (glue) ─────────────────────────────────────────────
|
||||
|
||||
describe("getHeartbeatGeoJob", () => {
|
||||
it("runs the geo pipeline", async () => {
|
||||
const { helper, defaults } = createHelper();
|
||||
|
||||
await helper.getHeartbeatGeoJob()(makeMonitor());
|
||||
|
||||
expect(defaults.geoCheckPipeline.run).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("catches and logs a pipeline error without rethrowing", async () => {
|
||||
const { helper, defaults } = createHelper({ geoCheckPipeline: { run: jest.fn().mockRejectedValue(new Error("api timeout")) } });
|
||||
|
||||
await expect(helper.getHeartbeatGeoJob()(makeMonitor())).resolves.toBeUndefined();
|
||||
expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "api timeout" }));
|
||||
});
|
||||
|
||||
it("logs 'Unknown error' for non-Error failures", async () => {
|
||||
const { helper, defaults } = createHelper({ geoCheckPipeline: { run: jest.fn().mockRejectedValue("string error") } });
|
||||
|
||||
await helper.getHeartbeatGeoJob()(makeMonitor());
|
||||
|
||||
expect(defaults.logger.error).toHaveBeenCalledWith(expect.objectContaining({ message: "Unknown error", stack: undefined }));
|
||||
});
|
||||
});
|
||||
|
||||
// ── getCleanupOrphanedJob ────────────────────────────────────────────────
|
||||
|
||||
describe("getCleanupOrphanedJob", () => {
|
||||
|
||||
Reference in new issue
Block a user