T-HUB/packages/scheduling/src/cron.test.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

99 lines
3.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { parseCron, cronMatchesTime } from "./cron.js";
import { ImpossibleScheduleError, InvalidCronError } from "./errors.js";
describe("parseCron — valid syntax", () => {
it("parses the every-minute expression", () => {
const c = parseCron("* * * * *");
expect(c.minute.values.size).toBe(60);
expect(c.minute.restricted).toBe(false);
expect(c.dayOfMonth.restricted).toBe(false);
expect(c.dayOfWeek.restricted).toBe(false);
});
it("parses steps, ranges and lists", () => {
const c = parseCron("*/15 8-18/2 * * 1,3,5");
expect([...c.minute.values]).toEqual([0, 15, 30, 45]);
expect([...c.hour.values]).toEqual([8, 10, 12, 14, 16, 18]);
expect([...c.dayOfWeek.values]).toEqual([1, 3, 5]);
expect(c.minute.restricted).toBe(true);
expect(c.hour.restricted).toBe(true);
expect(c.dayOfWeek.restricted).toBe(true);
});
it("parses month and dow names case-insensitively", () => {
const c = parseCron("0 0 1 jan-mar mon-fri");
expect([...c.month.values]).toEqual([1, 2, 3]);
expect([...c.dayOfWeek.values]).toEqual([1, 2, 3, 4, 5]);
});
it("accepts a list element of one value", () => {
expect([...parseCron("5,10 * * * *").minute.values]).toEqual([5, 10]);
});
it("treats n/step as n..max/step", () => {
expect([...parseCron("20/15 * * * *").minute.values]).toEqual([20, 35, 50]);
});
});
describe("parseCron — rejection", () => {
const bad = [
"",
" ",
"* * * *",
"* * * * * *",
"60 * * * *",
"* 24 * * *",
"* * 0 * *",
"* * 32 * *",
"* * * 0 *",
"* * * 13 *",
"* * * * 7",
"* * * * MON-BR",
"a * * * *",
"*/0 * * * *",
"*/-1 * * * *",
"5-1 * * * *",
"1--2 * * * *",
"@reboot",
"@daily",
"? * * * *",
"* * ? * *",
"* * * * * 2025",
"0 0 * * MON#1",
"0 0 L * *",
"0 0 1 W *",
"1/2/3 * * * *",
"*/ * * * *",
"1,,2 * * * *",
"-5 * * * *",
];
it.each(bad.map((e) => [e]))("rejects %s", (expr) => {
expect(() => parseCron(expr)).toThrow(InvalidCronError);
});
it("rejects non-string input", () => {
expect(() => parseCron(undefined as unknown as string)).toThrow(InvalidCronError);
expect(() => parseCron(5 as unknown as string)).toThrow(InvalidCronError);
});
it("rejects impossible schedules", () => {
expect(() => parseCron("0 0 31 2 *")).toThrow(ImpossibleScheduleError);
expect(() => parseCron("0 0 30 2 *")).toThrow(ImpossibleScheduleError);
expect(() => parseCron("0 0 31 4 *")).toThrow(ImpossibleScheduleError);
});
it("accepts Feb 29 (leap years exist)", () => {
expect(() => parseCron("0 0 29 2 *")).not.toThrow();
});
it("accepts the 31st in long months", () => {
expect(() => parseCron("0 0 31 1 *")).not.toThrow();
expect(() => parseCron("0 0 31 * *")).not.toThrow();
});
});
describe("cronMatchesTime", () => {
const cron = parseCron("*/15 9 * * *");
it("matches on the quarter hour in the 9 hour", () => {
expect(cronMatchesTime(cron, 9, 0, 0)).toBe(true);
expect(cronMatchesTime(cron, 9, 45, 0)).toBe(true);
});
it("rejects other minutes/hours and any nonzero second", () => {
expect(cronMatchesTime(cron, 9, 20, 0)).toBe(false);
expect(cronMatchesTime(cron, 10, 0, 0)).toBe(false);
expect(cronMatchesTime(cron, 9, 0, 1)).toBe(false);
});
});