Initial Commit
This commit is contained in:
251
packages/foundation/timing/cron/tests/vectors.test.ts
Normal file
251
packages/foundation/timing/cron/tests/vectors.test.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { parseCron } from "../src/index.js";
|
||||
|
||||
interface Vector {
|
||||
expr: string;
|
||||
after: number;
|
||||
tz: string;
|
||||
expected: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const utc = (y: number, mo: number, d: number, h = 0, mi = 0, s = 0): number =>
|
||||
Date.UTC(y, mo, d, h, mi, s);
|
||||
|
||||
const vectors: Vector[] = [
|
||||
// Hourly variants
|
||||
{
|
||||
expr: "0 * * * *",
|
||||
after: utc(2026, 4, 11, 10, 30),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 11, 0),
|
||||
label: "hourly @00",
|
||||
},
|
||||
{
|
||||
expr: "*/15 * * * *",
|
||||
after: utc(2026, 4, 11, 10, 7),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 10, 15),
|
||||
label: "every 15 minutes",
|
||||
},
|
||||
{
|
||||
expr: "*/15 * * * *",
|
||||
after: utc(2026, 4, 11, 10, 45),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 11, 0),
|
||||
label: "every 15 wrap",
|
||||
},
|
||||
{
|
||||
expr: "*/30 * * * *",
|
||||
after: utc(2026, 4, 11, 10, 29),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 10, 30),
|
||||
label: "every 30 minutes",
|
||||
},
|
||||
|
||||
// Daily
|
||||
{
|
||||
expr: "0 9 * * *",
|
||||
after: utc(2026, 4, 11, 8, 0),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 9, 0),
|
||||
label: "daily 9am",
|
||||
},
|
||||
{
|
||||
expr: "0 0 * * *",
|
||||
after: utc(2026, 4, 11, 12, 0),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 12, 0, 0),
|
||||
label: "daily midnight",
|
||||
},
|
||||
{
|
||||
expr: "@daily",
|
||||
after: utc(2026, 4, 11, 12, 0),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 12, 0, 0),
|
||||
label: "@daily macro",
|
||||
},
|
||||
{
|
||||
expr: "@hourly",
|
||||
after: utc(2026, 4, 11, 10, 30),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 11, 0),
|
||||
label: "@hourly macro",
|
||||
},
|
||||
|
||||
// Weekdays / weekends
|
||||
// 2026-05-09 is Saturday. Next 9am weekday = Mon 2026-05-11.
|
||||
{
|
||||
expr: "0 9 * * MON-FRI",
|
||||
after: utc(2026, 4, 9),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 9, 0),
|
||||
label: "9am weekdays",
|
||||
},
|
||||
{
|
||||
expr: "0 12 * * 0",
|
||||
after: utc(2026, 4, 11),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 17, 12, 0),
|
||||
label: "noon Sundays",
|
||||
},
|
||||
{
|
||||
expr: "0 9 * * 7",
|
||||
after: utc(2026, 4, 11),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 17, 9, 0),
|
||||
label: "9am Sundays via 7",
|
||||
},
|
||||
|
||||
// Monthly / yearly
|
||||
{
|
||||
expr: "0 0 1 * *",
|
||||
after: utc(2026, 4, 11),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 5, 1, 0, 0),
|
||||
label: "first of month",
|
||||
},
|
||||
{
|
||||
expr: "@monthly",
|
||||
after: utc(2026, 4, 11),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 5, 1, 0, 0),
|
||||
label: "@monthly macro",
|
||||
},
|
||||
{
|
||||
expr: "0 0 1 1 *",
|
||||
after: utc(2026, 0, 2),
|
||||
tz: "UTC",
|
||||
expected: utc(2027, 0, 1, 0, 0),
|
||||
label: "annual Jan 1",
|
||||
},
|
||||
{
|
||||
expr: "@yearly",
|
||||
after: utc(2026, 0, 2),
|
||||
tz: "UTC",
|
||||
expected: utc(2027, 0, 1, 0, 0),
|
||||
label: "@yearly macro",
|
||||
},
|
||||
|
||||
// Month aliases
|
||||
{
|
||||
expr: "0 0 1 JUL *",
|
||||
after: utc(2026, 4, 11),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 6, 1, 0, 0),
|
||||
label: "month alias JUL",
|
||||
},
|
||||
|
||||
// 4 specific minutes
|
||||
{
|
||||
expr: "0,15,30,45 9-17 * * MON-FRI",
|
||||
after: utc(2026, 4, 11, 9, 16),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 9, 30),
|
||||
label: "quarter-hour business hours",
|
||||
},
|
||||
{
|
||||
expr: "0,15,30,45 9-17 * * MON-FRI",
|
||||
after: utc(2026, 4, 11, 17, 46),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 12, 9, 0),
|
||||
label: "wrap to next business day",
|
||||
},
|
||||
|
||||
// Range/step variations
|
||||
{
|
||||
expr: "0 10-12/2 * * *",
|
||||
after: utc(2026, 4, 11, 9, 0),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 10, 0),
|
||||
label: "step in range first",
|
||||
},
|
||||
{
|
||||
expr: "0 10-12/2 * * *",
|
||||
after: utc(2026, 4, 11, 11, 0),
|
||||
tz: "UTC",
|
||||
expected: utc(2026, 4, 11, 12, 0),
|
||||
label: "step in range second",
|
||||
},
|
||||
|
||||
// Leap year
|
||||
{
|
||||
expr: "0 0 29 2 *",
|
||||
after: utc(2026, 0, 1),
|
||||
tz: "UTC",
|
||||
expected: utc(2028, 1, 29, 0, 0),
|
||||
label: "leap day next",
|
||||
},
|
||||
|
||||
// DST — America/New_York 2026-03-08 spring-forward
|
||||
{
|
||||
expr: "0 3 * * *",
|
||||
after: utc(2026, 2, 8, 6, 0),
|
||||
tz: "America/New_York",
|
||||
// 03:00 EDT on 2026-03-08 = 07:00 UTC
|
||||
expected: utc(2026, 2, 8, 7, 0),
|
||||
label: "NY 3am day-of-spring-forward",
|
||||
},
|
||||
|
||||
// Europe/London non-DST anchor
|
||||
// 2026-01-01 09:00 London = 09:00 UTC (GMT in January)
|
||||
{
|
||||
expr: "0 9 * * *",
|
||||
after: utc(2025, 11, 31, 23, 0),
|
||||
tz: "Europe/London",
|
||||
expected: utc(2026, 0, 1, 9, 0),
|
||||
label: "London winter 9am",
|
||||
},
|
||||
|
||||
// Australia/Sydney winter 12:00 local = 02:00 UTC (UTC+10).
|
||||
// after = 2026-05-31T00:00Z = 2026-05-31T10:00 Sydney, next noon
|
||||
// Sydney = 2026-05-31T12:00 local = 2026-05-31T02:00 UTC.
|
||||
{
|
||||
expr: "0 12 * * *",
|
||||
after: utc(2026, 4, 31, 0, 0),
|
||||
tz: "Australia/Sydney",
|
||||
expected: utc(2026, 4, 31, 2, 0),
|
||||
label: "Sydney winter noon",
|
||||
},
|
||||
|
||||
// Seconds + Quartz variants (need seconds: true)
|
||||
// Tested separately because parseCron options differ.
|
||||
];
|
||||
|
||||
describe("vectors — 5-field UTC + DST", () => {
|
||||
for (const v of vectors) {
|
||||
it(v.label, { timeout: 5000 }, () => {
|
||||
const expr = parseCron(v.expr);
|
||||
const next = expr.nextRun(v.after, v.tz);
|
||||
expect(next).toBe(v.expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("vectors — 6-field seconds", () => {
|
||||
it("every 10 seconds", { timeout: 5000 }, () => {
|
||||
const expr = parseCron("*/10 * * * * *", { seconds: true });
|
||||
expect(expr.nextRun(utc(2026, 4, 11, 10, 0, 5), "UTC")).toBe(
|
||||
utc(2026, 4, 11, 10, 0, 10)
|
||||
);
|
||||
});
|
||||
|
||||
it("seconds with hour wrap", { timeout: 5000 }, () => {
|
||||
const expr = parseCron("30 0 * * * *", { seconds: true });
|
||||
expect(expr.nextRun(utc(2026, 4, 11, 10, 0, 30), "UTC")).toBe(
|
||||
utc(2026, 4, 11, 11, 0, 30)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("vectors — 7-field Quartz year", () => {
|
||||
it("year-constrained next run", { timeout: 5000 }, () => {
|
||||
const expr = parseCron("0 0 12 1 1 * 2030", {
|
||||
seconds: true,
|
||||
years: true,
|
||||
});
|
||||
expect(expr.nextRun(utc(2026, 0, 1), "UTC")).toBe(
|
||||
utc(2030, 0, 1, 12, 0, 0)
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user