From 398e5d7314d7e88f1ee1192bebc8f2b25375312c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 18 Jun 2026 09:57:05 -0700 Subject: [PATCH] add tests --- .../test/unit/services/bufferService.test.ts | 34 +++++++++++++++++-- .../test/unit/services/dbQueueWorker.test.ts | 14 +++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server/test/unit/services/bufferService.test.ts b/server/test/unit/services/bufferService.test.ts index 5a6418c70..7c344c086 100644 --- a/server/test/unit/services/bufferService.test.ts +++ b/server/test/unit/services/bufferService.test.ts @@ -4,6 +4,7 @@ import { createMockLogger } from "../../helpers/createMockLogger.ts"; import type { ICheckService } from "../../../src/domain/checks/check.service.ts"; import type { IGeoChecksService } from "../../../src/domain/geo-checks/geo-check.service.ts"; import type { ISettingsService } from "../../../src/domain/app-settings/app-settings.service.ts"; +import type { IJobsRepository } from "../../../src/domain/jobs/job.repository.interface.ts"; import type { Check } from "../../../src/domain/checks/check.type.ts"; import type { GeoCheck } from "../../../src/domain/geo-checks/geo-check.type.ts"; @@ -24,6 +25,11 @@ const createMockSettingsService = (nodeEnv: string = "development") => getSettings: jest.fn().mockReturnValue({ nodeEnv }), }) as unknown as jest.Mocked; +const createMockJobsRepository = () => + ({ + upsertEvaluate: jest.fn().mockResolvedValue(true), + }) as unknown as jest.Mocked; + const makeCheck = (overrides?: Partial): Check => ({ id: "check-1", @@ -47,8 +53,9 @@ const createService = (nodeEnv: string = "development") => { const checkService = createMockCheckService(); const geoChecksService = createMockGeoChecksService(); const settingsService = createMockSettingsService(nodeEnv); - const service = new BufferService(logger as any, checkService, geoChecksService, settingsService); - return { service, logger, checkService, geoChecksService }; + const jobsRepository = createMockJobsRepository(); + const service = new BufferService(logger as any, checkService, geoChecksService, settingsService, jobsRepository); + return { service, logger, checkService, geoChecksService, jobsRepository }; }; // ── Tests ──────────────────────────────────────────────────────────────────── @@ -344,6 +351,29 @@ describe("BufferService", () => { ); }); + it("arms the evaluate stage once per distinct monitor after the checks are stored", async () => { + const { service, jobsRepository } = createService(); + service.addToBuffer(makeCheck({ id: "c1", metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" } })); + service.addToBuffer(makeCheck({ id: "c2", metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" } })); + service.addToBuffer(makeCheck({ id: "c3", metadata: { monitorId: "mon-2", teamId: "team-1", type: "http" } })); + + await service.flushBuffer(); + + expect(jobsRepository.upsertEvaluate).toHaveBeenCalledTimes(2); + expect(jobsRepository.upsertEvaluate).toHaveBeenCalledWith("mon-1", expect.any(Number)); + expect(jobsRepository.upsertEvaluate).toHaveBeenCalledWith("mon-2", expect.any(Number)); + }); + + it("does not arm the evaluate stage when the check write fails", async () => { + const { service, checkService, jobsRepository } = createService(); + (checkService.createChecks as jest.Mock).mockRejectedValue(new Error("DB write failed")); + service.addToBuffer(makeCheck()); + + await service.flushBuffer(); + + expect(jobsRepository.upsertEvaluate).not.toHaveBeenCalled(); + }); + it("clears buffer even on error to prevent infinite retries", async () => { const { service, checkService, logger } = createService(); (checkService.createChecks as jest.Mock).mockRejectedValue(new Error("DB write failed")); diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts index e24c5790d..e60f82a95 100644 --- a/server/test/unit/services/dbQueueWorker.test.ts +++ b/server/test/unit/services/dbQueueWorker.test.ts @@ -270,7 +270,8 @@ describe("DBQueueWorker", () => { expect(mocks.monitorsRepository.findByIds).toHaveBeenCalledWith(["m1"]); expect(mocks.checkProducer.produce).toHaveBeenCalled(); - expect(mocks.jobsRepository.upsertEvaluate).toHaveBeenCalledWith("m1", expect.any(Number)); + // The check stage only produces; the buffer arms evaluate once the check is durably stored + expect(mocks.jobsRepository.upsertEvaluate).not.toHaveBeenCalled(); expect(mocks.jobsRepository.recordSuccess).toHaveBeenCalledWith(job.id, job.nextScheduledAt, job.intervalMs, expect.any(Number)); expect(mocks.jobsRepository.recordFailure).not.toHaveBeenCalled(); }); @@ -394,6 +395,17 @@ describe("DBQueueWorker", () => { expect(types).toContain("geo-check"); }); + it("addJob schedules new jobs to run immediately (no startup jitter)", async () => { + const { worker, mocks } = createWorker(); + active = worker; + const now = Date.now(); + await worker.addJob("m1", makeMonitor({ geoCheckEnabled: true })); + + for (const [seed] of mocks.jobsRepository.upsertJob.mock.calls as any[][]) { + expect(seed.nextScheduledAt).toBe(now); + } + }); + it("addJob seeds only a check row for non-geo monitors", async () => { const { worker, mocks } = createWorker(); active = worker;