mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
feat(automations): add per-host maintenance mode and recurring schedules (#1521)
This commit is contained in:
1 parent
fd91d4925a
commit
99cffc9ba6
26 files changed
+1813
-6
No files matched your search
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
advanceMaintenance,
|
||||
beginMaintenance,
|
||||
nextOccurrence,
|
||||
parsePlan,
|
||||
plansOverlap,
|
||||
} from "../../src/backend/maintenance-time.js";
|
||||
import {
|
||||
emptyMaintenance,
|
||||
type MaintenancePlan,
|
||||
} from "../../src/maintenance.js";
|
||||
|
||||
const at = (value: string) => Date.parse(value);
|
||||
const plan = (overrides: Partial<MaintenancePlan> = {}): MaintenancePlan => ({
|
||||
id: "one",
|
||||
reason: "Upgrade",
|
||||
start: "2026-01-31T21:00:00.000Z",
|
||||
nextStart: "2026-01-31T21:00:00.000Z",
|
||||
durationMinutes: 60,
|
||||
recurrence: "once",
|
||||
graceMinutes: 20,
|
||||
notifyOverdue: true,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("maintenance calendar", () => {
|
||||
it("keeps weekly UTC time across DST and skips missing monthly days", () => {
|
||||
expect(
|
||||
new Date(
|
||||
nextOccurrence(
|
||||
plan({ recurrence: "weekly" }),
|
||||
at("2026-03-28T21:00Z"),
|
||||
)!,
|
||||
).toISOString(),
|
||||
).toBe("2026-04-04T21:00:00.000Z");
|
||||
expect(
|
||||
new Date(
|
||||
nextOccurrence(
|
||||
plan({ recurrence: "monthly" }),
|
||||
at("2026-01-31T21:00Z"),
|
||||
)!,
|
||||
).toISOString(),
|
||||
).toBe("2026-03-31T21:00:00.000Z");
|
||||
expect(nextOccurrence(plan(), at("2026-01-31T21:00Z"))).toBeNull();
|
||||
});
|
||||
it("rejects actual overlaps and accepts touching windows", () => {
|
||||
expect(plansOverlap(plan(), plan({ start: "2026-01-31T21:59:00Z" }))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(plansOverlap(plan(), plan({ start: "2026-01-31T22:00:00Z" }))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
plansOverlap(
|
||||
plan({ recurrence: "monthly" }),
|
||||
plan({ start: "2030-03-31T21:30:00Z" }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
plansOverlap(
|
||||
plan({ recurrence: "weekly" }),
|
||||
plan({ recurrence: "monthly", start: "2026-03-31T21:30:00Z" }),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
plansOverlap(
|
||||
plan({ recurrence: "weekly" }),
|
||||
plan({ recurrence: "monthly", start: "2026-03-31T23:00:00Z" }),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
it("does not end at ETM and merges a later nonoverlapping schedule", () => {
|
||||
const state = emptyMaintenance();
|
||||
state.plans = [
|
||||
plan(),
|
||||
plan({
|
||||
id: "two",
|
||||
reason: "Firmware",
|
||||
start: "2026-01-31T22:30:00Z",
|
||||
nextStart: "2026-01-31T22:30:00Z",
|
||||
}),
|
||||
];
|
||||
advanceMaintenance(state, at("2026-01-31T22:15Z"));
|
||||
expect(state.active?.estimatedEnd).toBe("2026-01-31T22:00:00.000Z");
|
||||
advanceMaintenance(state, at("2026-01-31T22:45Z"));
|
||||
expect(state.active?.startedAt).toBe("2026-01-31T21:00:00.000Z");
|
||||
expect(state.active?.estimatedEnd).toBe("2026-01-31T23:30:00.000Z");
|
||||
expect(state.active?.reasons).toEqual(["Upgrade", "Firmware"]);
|
||||
advanceMaintenance(state, at("2026-02-02T00:00Z"));
|
||||
expect(state.active).not.toBeNull();
|
||||
});
|
||||
it("catches up after restart and never re-enters a consumed one-time plan", () => {
|
||||
const state = emptyMaintenance();
|
||||
state.plans = [plan({ recurrence: "monthly" })];
|
||||
advanceMaintenance(state, at("2026-05-01T00:00Z"));
|
||||
expect(state.active?.startedAt).toBe("2026-03-31T21:00:00.000Z");
|
||||
expect(state.plans[0].nextStart).toBe("2026-05-31T21:00:00.000Z");
|
||||
state.active = null;
|
||||
advanceMaintenance(state, at("2026-05-01T00:00Z"));
|
||||
expect(state.active).toBeNull();
|
||||
});
|
||||
it("preserves a delivered reminder unless a later estimate extends maintenance", () => {
|
||||
const state = emptyMaintenance();
|
||||
beginMaintenance(state, plan(), at("2026-01-31T21:00Z"));
|
||||
state.active!.notified = true;
|
||||
beginMaintenance(
|
||||
state,
|
||||
plan({ durationMinutes: 10 }),
|
||||
at("2026-01-31T21:05Z"),
|
||||
);
|
||||
expect(state.active!.notified).toBe(true);
|
||||
beginMaintenance(state, plan(), at("2026-01-31T22:30Z"));
|
||||
expect(state.active!.notified).toBe(false);
|
||||
});
|
||||
it.each([
|
||||
{ reason: " " },
|
||||
{ durationMinutes: 0 },
|
||||
{ durationMinutes: 10081 },
|
||||
{ graceMinutes: -1 },
|
||||
{ recurrence: "daily" },
|
||||
{ notifyOverdue: "true" },
|
||||
{ start: "2020-01-01T00:00Z" },
|
||||
{ start: "2026-01-31T21:00:01Z" },
|
||||
])("rejects invalid input %j", (override) => {
|
||||
expect(() =>
|
||||
parsePlan(plan(override as never), at("2026-01-01T00:00Z")),
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user