diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts index 12b46de95..376b8f20d 100644 --- a/server/test/unit/services/maintenanceWindowService.test.ts +++ b/server/test/unit/services/maintenanceWindowService.test.ts @@ -296,6 +296,45 @@ describe("MaintenanceWindowService", () => { expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1); expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]); }); + + it("skips the overlap lookup when an active window with no monitorIds is deleted", async () => { + const maintenanceWindowsRepository = createMaintenanceWindowsRepo(); + (maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: [] })); + const { service, monitorsRepository } = createService({ maintenanceWindowsRepository }); + + await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); + + expect(maintenanceWindowsRepository.findByMonitorIds).not.toHaveBeenCalled(); + expect(monitorsRepository.updateByIds).not.toHaveBeenCalled(); + }); + + it("ignores monitorIds in overlapping windows that are outside the leaving set", async () => { + const maintenanceWindowsRepository = createMaintenanceWindowsRepo(); + (maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] })); + // overlapping window covers mon-1 (in the leaving set) plus mon-99 (unrelated, must be skipped) + (maintenanceWindowsRepository.findByMonitorIds as jest.Mock).mockResolvedValue([ + makeActiveWindow({ id: "mw-2", monitorIds: ["mon-1", "mon-99"] }), + ]); + const { service, monitorsRepository } = createService({ maintenanceWindowsRepository }); + + await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); + + // mon-1 is still covered by mw-2; mon-99 must not leak into the update + expect(monitorsRepository.updateByIds).not.toHaveBeenCalled(); + }); + + it("makes no monitor updates when every leaving monitor is still covered by another active window", async () => { + const maintenanceWindowsRepository = createMaintenanceWindowsRepo(); + (maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] })); + (maintenanceWindowsRepository.findByMonitorIds as jest.Mock).mockResolvedValue([ + makeActiveWindow({ id: "mw-2", monitorIds: ["mon-1", "mon-2"] }), + ]); + const { service, monitorsRepository } = createService({ maintenanceWindowsRepository }); + + await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" }); + + expect(monitorsRepository.updateByIds).not.toHaveBeenCalled(); + }); }); // ── editMaintenanceWindow ──────────────────────────────────────────────── diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index d2ee164bc..5e73ab77b 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -973,6 +973,79 @@ describe("MonitorService", () => { }) ); }); + + it("logs a resume failure with undefined stack when rejection is not an Error instance", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + const monitors = [makeMonitor({ id: "m1", isActive: true })]; + (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); + const jobQueue = createJobQueueMock(); + // Reject with a non-Error value to exercise the `instanceof Error` false branch + (jobQueue.resumeJob as jest.Mock).mockRejectedValueOnce("queue blew up"); + + const logger = createMockLogger(); + const { service } = createService({ monitorsRepository, jobQueue, logger }); + + const result = await service.bulkPauseMonitors({ + teamId: TEAM_ID, + monitorIds: ["m1"], + pause: false, + }); + + expect(result.failedCount).toBe(1); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith({ + message: "Failed to sync job queue for monitor m1 during bulk resume", + service: "MonitorService", + method: "bulkPauseMonitors", + stack: undefined, + }); + }); + + it("logs 'unknown' when the failing monitor has no id", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + const monitors = [makeMonitor({ id: "", isActive: false })]; + (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue(monitors); + const jobQueue = createJobQueueMock(); + (jobQueue.pauseJob as jest.Mock).mockRejectedValueOnce(new Error("boom")); + + const logger = createMockLogger(); + const { service } = createService({ monitorsRepository, jobQueue, logger }); + + const result = await service.bulkPauseMonitors({ + teamId: TEAM_ID, + monitorIds: ["m1"], + pause: true, + }); + + expect(result.failedCount).toBe(1); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Failed to sync job queue for monitor unknown during bulk pause", + }) + ); + }); + + it("logs 'unknown' when the failing monitor at the rejected index is nullish", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + // Defensive: bulkTogglePause returns an array containing a nullish entry; the + // inner map throws TypeError, the forEach hits the `?.` short-circuit branch. + (monitorsRepository.bulkTogglePause as jest.Mock).mockResolvedValue([undefined]); + const logger = createMockLogger(); + const { service } = createService({ monitorsRepository, logger }); + + const result = await service.bulkPauseMonitors({ + teamId: TEAM_ID, + monitorIds: ["m1"], + pause: true, + }); + + expect(result.failedCount).toBe(1); + expect(logger.error).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Failed to sync job queue for monitor unknown during bulk pause", + }) + ); + }); }); describe("deleteMonitor", () => {