Merge pull request #3668 from bluewave-labs/fix/update-monitors-on-maintenance-window-change

fix: update monitors on maintenance window change
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-05-21 11:18:59 -07:00
commit bec5ef09cb
9 files changed
+427 -32

No files matched your search

@@ -5,6 +5,7 @@ export interface IMaintenanceWindowsRepository {
// fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByMonitorIds(monitorIds: string[], teamId: string, excludeId?: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise<MaintenanceWindow[]>;
// update
@@ -65,6 +65,18 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
return this.toEntity(maintenanceWindow);
};
findByMonitorIds = async (monitorIds: string[], teamId: string, excludeId?: string): Promise<MaintenanceWindow[]> => {
const query: Record<string, unknown> = {
monitorIds: { $in: monitorIds },
teamId,
};
if (excludeId) {
query._id = { $ne: new mongoose.Types.ObjectId(excludeId) };
}
const maintenanceWindows = await MaintenanceWindowModel.find(query);
return this.mapDocuments(maintenanceWindows);
};
findByMonitorId = async (monitorId: string, teamId: string): Promise<MaintenanceWindow[]> => {
const maintenanceWindows = await MaintenanceWindowModel.find({
monitorIds: monitorId,
@@ -113,6 +125,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
if (!updated) {
throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 });
}
return this.toEntity(updated);
};
@@ -124,6 +137,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
if (!deleted) {
throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 });
}
return this.toEntity(deleted);
};
countByTeamId = async (teamId: string, active?: boolean) => {
@@ -1,4 +1,4 @@
import { type MonitorType, type Monitor, type MonitorsSummary, CheckSnapshot } from "@/types/index.js";
import { type MonitorType, type Monitor, type MonitorStatus, type MonitorsSummary, CheckSnapshot } from "@/types/index.js";
export interface TeamQueryConfig {
limit?: number;
@@ -31,6 +31,7 @@ export interface IMonitorsRepository {
// update
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
updateByIds(monitorIds: string[], teamId: string, updates: Partial<Monitor>, excludeStatuses?: MonitorStatus[]): Promise<number>;
updateStatusWindowAndChecks(
monitorId: string,
teamId: string,
@@ -1,6 +1,6 @@
import { MonitorModel } from "@/db/models/index.js";
import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js";
import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js";
import type { Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/types/index.js";
import mongoose, { type FilterQuery, type PipelineStage } from "mongoose";
import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
import { MongoBulkWriteError } from "mongodb";
@@ -190,6 +190,16 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.toEntity(updatedMonitor);
};
updateByIds = async (monitorIds: string[], teamId: string, updates: Partial<Monitor>, excludeStatuses?: MonitorStatus[]) => {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const filter: Record<string, unknown> = { _id: { $in: objectIds }, teamId };
if (excludeStatuses && excludeStatuses.length > 0) {
filter.status = { $nin: excludeStatuses };
}
const updated = await MonitorModel.updateMany(filter, { $set: updates }, { runValidators: true });
return updated.modifiedCount;
};
updateStatusWindowAndChecks = async (
monitorId: string,
teamId: string,
@@ -1,6 +1,7 @@
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { DurationUnit, MaintenanceWindow } from "@/types/index.js";
import { AppError } from "@/utils/AppError.js";
import { isWindowActive } from "@/utils/maintenanceWindow.js";
const SERVICE_NAME = "maintenanceWindowService";
@@ -54,6 +55,24 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
return MaintenanceWindowService.SERVICE_NAME;
}
private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise<void> => {
if (monitorIds.length === 0) return;
const otherWindows = await this.maintenanceWindowsRepository.findByMonitorIds(monitorIds, teamId, excludeWindowId);
const stillCovered = new Set<string>();
for (const otherWindow of otherWindows) {
if (!isWindowActive(otherWindow, now)) continue;
for (const monitorId of otherWindow.monitorIds) {
if (monitorIds.includes(monitorId)) stillCovered.add(monitorId);
}
}
const toInitializing = monitorIds.filter((monitorId) => !stillCovered.has(monitorId));
if (toInitializing.length > 0) {
await this.monitorsRepository.updateByIds(toInitializing, teamId, { status: "initializing" }, ["paused"]);
}
};
createMaintenanceWindow = async ({
teamId,
monitorIDs,
@@ -88,7 +107,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
});
}
await this.maintenanceWindowsRepository.create({
const created = await this.maintenanceWindowsRepository.create({
teamId,
monitorIds: monitorIDs,
name: name,
@@ -99,6 +118,10 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
start: start,
end: end,
});
if (isWindowActive(created)) {
await this.monitorsRepository.updateByIds(created.monitorIds, teamId, { status: "maintenance" }, ["paused"]);
}
};
getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => {
@@ -133,7 +156,14 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
};
deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => {
return await this.maintenanceWindowsRepository.deleteById(id, teamId);
const deleted = await this.maintenanceWindowsRepository.deleteById(id, teamId);
const now = new Date();
if (isWindowActive(deleted, now)) {
await this.flipMonitorsLeavingMaintenance(deleted.monitorIds, teamId, deleted.id, now);
}
return deleted;
};
editMaintenanceWindow = async ({
@@ -145,6 +175,8 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
teamId: string;
body: Partial<Omit<MaintenanceWindow, "monitorIds">> & { monitors?: string[] };
}) => {
const existing = await this.maintenanceWindowsRepository.findById(id, teamId);
const { monitors, ...rest } = body;
const update: Partial<MaintenanceWindow> = rest;
@@ -162,6 +194,24 @@ export class MaintenanceWindowService implements IMaintenanceWindowService {
update.monitorIds = monitors;
}
return await this.maintenanceWindowsRepository.updateById(id, teamId, update);
const updated = await this.maintenanceWindowsRepository.updateById(id, teamId, update);
const now = new Date();
const wasActive = isWindowActive(existing, now);
const isActive = isWindowActive(updated, now);
const enteringMaintenance = isActive ? updated.monitorIds.filter((monitorId) => !wasActive || !existing.monitorIds.includes(monitorId)) : [];
const leavingCandidates = wasActive ? existing.monitorIds.filter((monitorId) => !isActive || !updated.monitorIds.includes(monitorId)) : [];
if (enteringMaintenance.length > 0) {
await this.monitorsRepository.updateByIds(enteringMaintenance, teamId, { status: "maintenance" }, ["paused"]);
}
if (leavingCandidates.length > 0) {
await this.flipMonitorsLeavingMaintenance(leavingCandidates, teamId, existing.id, now);
}
return updated;
};
}
@@ -11,7 +11,8 @@ import {
IncidentService,
type IGeoChecksService,
} from "@/service/index.js";
import { CHECK_TTL_SENTINEL, type MaintenanceWindow, type StatusChangeResult } from "@/types/index.js";
import { CHECK_TTL_SENTINEL, type StatusChangeResult } from "@/types/index.js";
import { isWindowActive } from "@/utils/maintenanceWindow.js";
import {
IMaintenanceWindowsRepository,
IMonitorsRepository,
@@ -358,32 +359,8 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper {
async isInMaintenanceWindow(monitorId: string, teamId: string) {
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
// If start is < now and end > now, we're in maintenance
if (start <= now && end >= now) return true;
// If maintenance window was set in the past with a repeat,
// we need to advance start and end to see if we are in range
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return acc;
}
return acc;
}, false);
return maintenanceWindowIsActive;
const now = new Date();
return maintenanceWindows.some((window) => isWindowActive(window, now));
}
getCleanupRetentionJob = () => {
+28
View File
@@ -0,0 +1,28 @@
import type { MaintenanceWindow } from "@/types/index.js";
export const isWindowActive = (window: MaintenanceWindow, now: Date = new Date()): boolean => {
if (!window.active) {
return false;
}
const start = new Date(window.start);
const end = new Date(window.end);
const repeatInterval = window.repeat || 0;
if (start <= now && end >= now) {
return true;
}
// For a recurring window whose first occurrence is in the past,
// advance start/end by the repeat interval until we either catch
// an occurrence covering `now` or move past it.
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return false;
};
@@ -11,6 +11,7 @@ const createMaintenanceWindowsRepo = () =>
create: jest.fn().mockResolvedValue(makeWindow()),
findById: jest.fn().mockResolvedValue(makeWindow()),
findByMonitorId: jest.fn().mockResolvedValue([makeWindow()]),
findByMonitorIds: jest.fn().mockResolvedValue([]),
findByTeamId: jest.fn().mockResolvedValue([makeWindow()]),
updateById: jest.fn().mockResolvedValue(makeWindow()),
deleteById: jest.fn().mockResolvedValue(makeWindow()),
@@ -23,6 +24,7 @@ const createMonitorsRepo = () =>
{ id: "mon-1", teamId: "team-1" },
{ id: "mon-2", teamId: "team-1" },
]),
updateByIds: jest.fn().mockResolvedValue(0),
}) as unknown as jest.Mocked<IMonitorsRepository>;
const createService = (overrides?: {
@@ -51,6 +53,16 @@ const makeWindow = (overrides?: Partial<MaintenanceWindow>): MaintenanceWindow =
...overrides,
});
// A window whose start/end straddle "now" — active per isWindowActive
const makeActiveWindow = (overrides?: Partial<MaintenanceWindow>): MaintenanceWindow => {
const now = Date.now();
return makeWindow({
start: new Date(now - 60_000).toISOString(),
end: new Date(now + 60_000).toISOString(),
...overrides,
});
};
const defaultCreateParams = {
teamId: "team-1",
monitorIDs: ["mon-1", "mon-2"],
@@ -137,6 +149,26 @@ describe("MaintenanceWindowService", () => {
expect(maintenanceWindowsRepository.create).toHaveBeenCalledTimes(1);
expect(maintenanceWindowsRepository.create).toHaveBeenCalledWith(expect.objectContaining({ monitorIds: ["mon-1"] }));
});
it("flips monitors to maintenance when the created window is active", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.create as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.createMaintenanceWindow(defaultCreateParams);
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }, ["paused"]);
});
it("does not touch monitor status when the created window is inactive", async () => {
// default mock returns makeWindow() with past dates → inactive
const { service, monitorsRepository } = createService();
await service.createMaintenanceWindow(defaultCreateParams);
expect(monitorsRepository.updateByIds).not.toHaveBeenCalled();
});
});
// ── getMaintenanceWindowById ─────────────────────────────────────────────
@@ -221,6 +253,49 @@ describe("MaintenanceWindowService", () => {
expect(result).toBe(deleted);
expect(maintenanceWindowsRepository.deleteById).toHaveBeenCalledWith("mw-1", "team-1");
});
it("does not touch monitor status when deleting an inactive window", async () => {
// default fixture has past dates → inactive
const { service, monitorsRepository, maintenanceWindowsRepository } = createService();
await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" });
expect(monitorsRepository.updateByIds).not.toHaveBeenCalled();
expect(maintenanceWindowsRepository.findByMonitorIds).not.toHaveBeenCalled();
});
it("flips covered monitors to initializing when deleting an active window", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" });
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }, ["paused"]);
});
it("excludes the deleted window from the overlap check", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.deleteById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] }));
const { service } = createService({ maintenanceWindowsRepository });
await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" });
expect(maintenanceWindowsRepository.findByMonitorIds).toHaveBeenCalledWith(["mon-1"], "team-1", "mw-1");
});
it("does not flip a monitor 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-2"] })]);
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.deleteMaintenanceWindow({ id: "mw-1", teamId: "team-1" });
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]);
});
});
// ── editMaintenanceWindow ────────────────────────────────────────────────
@@ -302,5 +377,149 @@ describe("MaintenanceWindowService", () => {
expect(monitorsRepository.findByIds).not.toHaveBeenCalled();
});
// ── transitions ──────────────────────────────────────────────────
it("does not touch monitor status when window was inactive and stays inactive", async () => {
// default fixture: start/end in 2026-04-10 (past) → inactive
const { service, monitorsRepository } = createService();
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { name: "Updated Name" },
});
expect(monitorsRepository.updateByIds).not.toHaveBeenCalled();
});
it("sets all monitors to maintenance when window transitions inactive → active", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeWindow({ monitorIds: ["mon-1", "mon-2"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { start: new Date(Date.now() - 60_000).toISOString() },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "maintenance" }, ["paused"]);
});
it("sets all monitors to initializing when window transitions active → inactive", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"], active: false }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { active: false },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1", "mon-2"], "team-1", { status: "initializing" }, ["paused"]);
});
it("flips only newly added monitors to maintenance when window stays active", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { monitors: ["mon-1", "mon-2"] },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "maintenance" }, ["paused"]);
});
it("flips only removed monitors to initializing when window stays active", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { monitors: ["mon-1"] },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-2"], "team-1", { status: "initializing" }, ["paused"]);
});
it("makes no monitor updates when window stays active and monitor set is unchanged", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"], name: "Renamed" }));
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { name: "Renamed" },
});
expect(monitorsRepository.updateByIds).not.toHaveBeenCalled();
});
it("does not flip a leaving monitor to initializing if another active window still covers it", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1", "mon-2"], active: false }));
(maintenanceWindowsRepository.findByMonitorIds as jest.Mock).mockResolvedValue([makeActiveWindow({ id: "mw-2", monitorIds: ["mon-2"] })]);
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { active: false },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledTimes(1);
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]);
});
it("excludes the current window from the overlap check", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"], active: false }));
const { service } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { active: false },
});
expect(maintenanceWindowsRepository.findByMonitorIds).toHaveBeenCalledWith(["mon-1"], "team-1", "mw-1");
});
it("flips a leaving monitor to initializing if other windows covering it are inactive", async () => {
const maintenanceWindowsRepository = createMaintenanceWindowsRepo();
(maintenanceWindowsRepository.findById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"] }));
(maintenanceWindowsRepository.updateById as jest.Mock).mockResolvedValue(makeActiveWindow({ monitorIds: ["mon-1"], active: false }));
(maintenanceWindowsRepository.findByMonitorIds as jest.Mock).mockResolvedValue([
makeWindow({ id: "mw-2", monitorIds: ["mon-1"], active: false }),
]);
const { service, monitorsRepository } = createService({ maintenanceWindowsRepository });
await service.editMaintenanceWindow({
id: "mw-1",
teamId: "team-1",
body: { active: false },
});
expect(monitorsRepository.updateByIds).toHaveBeenCalledWith(["mon-1"], "team-1", { status: "initializing" }, ["paused"]);
});
});
});
@@ -0,0 +1,95 @@
import { describe, expect, it } from "@jest/globals";
import { isWindowActive } from "../../../src/utils/maintenanceWindow.ts";
import type { MaintenanceWindow } from "../../../src/types/index.ts";
const makeWindow = (overrides?: Partial<MaintenanceWindow>): MaintenanceWindow => {
const now = Date.now();
return {
id: "mw-1",
monitorIds: ["mon-1"],
teamId: "team-1",
active: true,
name: "Scheduled Maintenance",
duration: 60,
durationUnit: "minutes",
repeat: 0,
start: new Date(now - 1000).toISOString(),
end: new Date(now + 1000).toISOString(),
createdAt: "2026-01-01T00:00:00Z",
updatedAt: "2026-01-01T00:00:00Z",
...overrides,
};
};
describe("isWindowActive", () => {
it("returns false when window.active is false even if dates span now", () => {
expect(isWindowActive(makeWindow({ active: false }))).toBe(false);
});
it("returns true for a one-time window where now is between start and end", () => {
expect(isWindowActive(makeWindow())).toBe(true);
});
it("returns false when the one-time window has not started yet", () => {
const now = Date.now();
expect(
isWindowActive(
makeWindow({
start: new Date(now + 60_000).toISOString(),
end: new Date(now + 120_000).toISOString(),
})
)
).toBe(false);
});
it("returns false when the one-time window has already ended", () => {
const now = Date.now();
expect(
isWindowActive(
makeWindow({
start: new Date(now - 120_000).toISOString(),
end: new Date(now - 60_000).toISOString(),
repeat: 0,
})
)
).toBe(false);
});
it("returns true when a recurring window's advanced occurrence covers now", () => {
const now = Date.now();
// 2h ago, 10-min window, hourly repeat — third occurrence (0h) covers now
expect(
isWindowActive(
makeWindow({
start: new Date(now - 7_200_000).toISOString(),
end: new Date(now - 7_200_000 + 600_000).toISOString(),
repeat: 3_600_000,
})
)
).toBe(true);
});
it("returns false when a recurring window's occurrences all miss now", () => {
const now = Date.now();
// 31min ago, 1-min window, hourly repeat → next occurrence ~29min from now
expect(
isWindowActive(
makeWindow({
start: new Date(now - 1_860_000).toISOString(),
end: new Date(now - 1_800_000).toISOString(),
repeat: 3_600_000,
})
)
).toBe(false);
});
it("respects the explicit `now` parameter for comparisons", () => {
const win = makeWindow({
start: "2026-01-15T11:00:00Z",
end: "2026-01-15T13:00:00Z",
});
expect(isWindowActive(win, new Date("2026-01-15T12:00:00Z"))).toBe(true);
expect(isWindowActive(win, new Date("2026-01-15T14:00:00Z"))).toBe(false);
expect(isWindowActive(win, new Date("2026-01-15T10:00:00Z"))).toBe(false);
});
});