Add timezone-aware cron schedules (#1232)

* feat: add timezone-aware cron schedules

* chore: normalize schedule timezone property

* fix: reject blank schedule timezone in mcp

* Stabilize terminal resize browser repro

* Fix terminal resize repro row assertions

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Daniel Salmerón Amselem
2026-06-01 00:32:45 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 922a93af2f
commit 632c48fde3
17 changed files with 431 additions and 19 deletions
@@ -144,19 +144,19 @@ describe("terminal resize reflow repro (Paseo terminal)", () => {
it("snapshot-restored rows stay frozen at the snapshot width after the terminal grows", async () => {
await page.viewport(1600, 700);
const m = mount(560, 360); // ~70 cols
await waitFor(() => m.terminal.cols > 0);
await waitFor(() => m.terminal.cols > 40);
const narrowCols = m.terminal.cols;
// 1) Mid-stream snapshot arrives (server overflowed 256KB). It carries the
// long line wrapped at the server width as separate grid rows.
await renderSnapshotCommitted(m.runtime, buildWrappedSnapshot(narrowCols));
await waitFor(() => someRowContains(m.terminal, '"seq":1'));
await waitFor(() => someRowContains(m.terminal, "[15:30:00.123]"));
// 2) Normal post-snapshot streaming resumes (autowrap on -> soft-wrapped).
for (let seq = 90; seq <= 96; seq++) {
await writeCommitted(m.runtime, encodeTerminalOutput(pinoLine(seq)));
}
await waitFor(() => someRowContains(m.terminal, '"seq":96'));
await waitFor(() => someRowContains(m.terminal, "[15:30:36.123]"));
// 3) User resizes the terminal wider.
m.root.style.width = "1480px";
@@ -13,6 +13,7 @@ import {
export interface ScheduleCreateOptions extends ScheduleCommandOptions {
every?: string;
cron?: string;
timezone?: string;
name?: string;
target?: string;
provider?: string;
@@ -34,6 +35,7 @@ export async function runCreateCommand(
prompt,
every: options.every,
cron: options.cron,
timezone: options.timezone,
name: options.name,
target: options.target,
provider: options.provider,
@@ -21,6 +21,7 @@ export function createScheduleCommand(): Command {
.argument("<prompt>", "Prompt to run on the schedule")
.option("--every <duration>", "Fixed interval cadence (for example: 5m, 1h)")
.option("--cron <expr>", "Cron cadence expression")
.option("--timezone <iana>", "IANA time zone for cron cadence (default: UTC)")
.option("--name <name>", "Optional schedule name")
.option("--target <self|new-agent|agent-id>", "Run target")
.option(
@@ -82,6 +83,7 @@ export function createScheduleCommand(): Command {
.argument("<id>", "Schedule ID")
.option("--every <duration>", "Switch to fixed interval cadence (for example: 5m, 1h)")
.option("--cron <expr>", "Switch to cron cadence expression")
.option("--timezone <iana>", "IANA time zone for cron cadence (requires --cron)")
.option("--name <name>", "Rename the schedule (empty string clears the name)")
.option("--prompt <text>", "Replace the schedule prompt")
.option(
+2 -2
View File
@@ -1,5 +1,5 @@
import type { OutputSchema } from "../../output/index.js";
import { formatTarget, type ScheduleRow } from "./shared.js";
import { formatCadence, formatTarget, type ScheduleRow } from "./shared.js";
import type { ScheduleRecord, ScheduleRunRecord } from "./types.js";
export const scheduleSchema: OutputSchema<ScheduleRow> = {
@@ -73,7 +73,7 @@ export function createScheduleInspectRows(schedule: ScheduleRecord): ScheduleIns
key: "Cadence",
value:
schedule.cadence.type === "cron"
? `cron:${schedule.cadence.expression}`
? formatCadence(schedule.cadence)
: `every:${schedule.cadence.everyMs}ms`,
},
{ key: "Target", value: formatTarget(schedule.target) },
@@ -88,6 +88,19 @@ describe("parseScheduleCreateInput first-run timing", () => {
expect(input.runOnCreate).toBe(true);
});
test("--cron with --timezone stores a timezone-aware cadence", () => {
const input = parseScheduleCreateInput({
...baseCron,
timezone: " America/New_York ",
});
expect(input.cadence).toEqual({
type: "cron",
expression: "0 9 * * *",
timezone: "America/New_York",
});
});
test("--every with --run-now is rejected as redundant", () => {
expect(() => parseScheduleCreateInput({ ...baseOptions, runNow: true })).toThrow(
expect.objectContaining({
@@ -105,6 +118,15 @@ describe("parseScheduleCreateInput first-run timing", () => {
}),
);
});
test("--timezone without --cron is rejected", () => {
expect(() => parseScheduleCreateInput({ ...baseOptions, timezone: "Europe/Zurich" })).toThrow(
expect.objectContaining({
code: "INVALID_TIME_ZONE",
message: "--timezone can only be used with --cron",
}),
);
});
});
describe("parseScheduleUpdateInput", () => {
@@ -151,12 +173,31 @@ describe("parseScheduleUpdateInput", () => {
});
});
test("parses --cron cadence with --timezone", () => {
expect(
parseScheduleUpdateInput({
id: "abc",
cron: "30 9 * * *",
timezone: "Europe/Zurich",
}),
).toEqual({
id: "abc",
cadence: { type: "cron", expression: "30 9 * * *", timezone: "Europe/Zurich" },
});
});
test("rejects passing both --every and --cron", () => {
expect(() => parseScheduleUpdateInput({ id: "abc", every: "5m", cron: "0 9 * * *" })).toThrow(
expect.objectContaining({ code: "INVALID_CADENCE" }),
);
});
test("rejects --timezone without --cron", () => {
expect(() => parseScheduleUpdateInput({ id: "abc", timezone: "Europe/Zurich" })).toThrow(
expect.objectContaining({ code: "INVALID_TIME_ZONE" }),
);
});
test("parses provider/model shorthand and explicit mode", () => {
expect(
parseScheduleUpdateInput({
+33 -4
View File
@@ -49,7 +49,8 @@ export function toScheduleCommandError(code: string, action: string, error: unkn
export function formatCadence(cadence: ScheduleCadence): string {
if (cadence.type === "cron") {
return `cron:${cadence.expression}`;
const timezoneSuffix = cadence.timezone ? ` (${cadence.timezone})` : "";
return `cron:${cadence.expression}${timezoneSuffix}`;
}
return `every:${formatDurationMs(cadence.everyMs)}`;
}
@@ -129,6 +130,7 @@ export function parseScheduleCreateInput(options: {
prompt: string;
every?: string;
cron?: string;
timezone?: string;
name?: string;
target?: string;
provider?: string;
@@ -147,7 +149,7 @@ export function parseScheduleCreateInput(options: {
} satisfies CommandError;
}
const cadence = parseCadenceFromFlags(options.every, options.cron);
const cadence = parseCadenceFromFlags(options.every, options.cron, options.timezone);
if (!cadence) {
throw {
code: "INVALID_CADENCE",
@@ -232,6 +234,7 @@ export interface ScheduleUpdateOptionsInput {
id: string;
every?: string;
cron?: string;
timezone?: string;
name?: string;
prompt?: string;
provider?: string;
@@ -253,7 +256,7 @@ export function parseScheduleUpdateInput(options: ScheduleUpdateOptionsInput): U
} satisfies CommandError;
}
const cadence = parseCadenceFromFlags(options.every, options.cron);
const cadence = parseCadenceFromFlags(options.every, options.cron, options.timezone);
const newAgentConfig = buildNewAgentConfigPatch(options);
const maxRuns = parseUpdateMaxRuns(options);
const expiresAt = parseUpdateExpiresAt(options);
@@ -288,6 +291,7 @@ export function parseScheduleUpdateInput(options: ScheduleUpdateOptionsInput): U
function parseCadenceFromFlags(
every: string | undefined,
cron: string | undefined,
timezone: string | undefined,
): ScheduleCadence | undefined {
if (every !== undefined && cron !== undefined) {
throw {
@@ -295,15 +299,40 @@ function parseCadenceFromFlags(
message: "Specify at most one of --every or --cron",
} satisfies CommandError;
}
const trimmedTimeZone = parseTimeZoneFlag(timezone);
if (trimmedTimeZone !== undefined && cron === undefined) {
throw {
code: "INVALID_TIME_ZONE",
message: "--timezone can only be used with --cron",
} satisfies CommandError;
}
if (every !== undefined) {
return { type: "every", everyMs: parseDuration(every) };
}
if (cron !== undefined) {
return { type: "cron", expression: cron.trim() };
return {
type: "cron",
expression: cron.trim(),
...(trimmedTimeZone ? { timezone: trimmedTimeZone } : {}),
};
}
return undefined;
}
function parseTimeZoneFlag(timeZone: string | undefined): string | undefined {
if (timeZone === undefined) {
return undefined;
}
const trimmed = timeZone.trim();
if (!trimmed) {
throw {
code: "INVALID_TIME_ZONE",
message: "--timezone cannot be empty",
} satisfies CommandError;
}
return trimmed;
}
function parseUpdateMaxRuns(options: ScheduleUpdateOptionsInput): number | null | undefined {
if (options.maxRuns !== undefined && options.clearMaxRuns) {
throw {
@@ -8,6 +8,7 @@ export type ScheduleCadence =
| {
type: "cron";
expression: string;
timezone?: string;
};
export type ScheduleTarget =
@@ -15,6 +15,7 @@ import {
export interface ScheduleUpdateOptions extends ScheduleCommandOptions {
every?: string;
cron?: string;
timezone?: string;
name?: string;
prompt?: string;
provider?: string;
@@ -36,6 +37,7 @@ export async function runUpdateCommand(
id,
every: options.every,
cron: options.cron,
timezone: options.timezone,
name: options.name,
prompt: options.prompt,
provider: options.provider,
+2
View File
@@ -568,6 +568,7 @@ export interface CreateScheduleOptions {
| {
type: "cron";
expression: string;
timezone?: string;
};
target:
| {
@@ -623,6 +624,7 @@ export interface UpdateScheduleOptions {
| {
type: "cron";
expression: string;
timezone?: string;
};
newAgentConfig?: UpdateScheduleNewAgentConfig;
maxRuns?: number | null;
@@ -0,0 +1,26 @@
import { describe, expect, test } from "vitest";
import { ScheduleCadenceSchema } from "./types.js";
describe("ScheduleCadenceSchema", () => {
test("accepts existing UTC cron cadence without a time zone", () => {
expect(ScheduleCadenceSchema.parse({ type: "cron", expression: "0 9 * * *" })).toEqual({
type: "cron",
expression: "0 9 * * *",
});
});
test("accepts timezone-aware cron cadence", () => {
expect(
ScheduleCadenceSchema.parse({
type: "cron",
expression: "0 9 * * *",
timezone: "America/New_York",
}),
).toEqual({
type: "cron",
expression: "0 9 * * *",
timezone: "America/New_York",
});
});
});
+1
View File
@@ -12,6 +12,7 @@ export const ScheduleCadenceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("cron"),
expression: z.string().trim().min(1),
timezone: z.string().trim().min(1).optional(),
}),
]);
export type ScheduleCadence = z.infer<typeof ScheduleCadenceSchema>;
@@ -2328,6 +2328,38 @@ describe("create_schedule MCP tool", () => {
);
});
it("passes timezone through cron create_schedule input", async () => {
const { agentManager, agentStorage } = createTestDeps();
const create = vi.fn(async (scheduleInput: CreateScheduleInput) =>
createStoredSchedule(scheduleInput),
);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { create } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "create_schedule");
await invokeToolWithParsedInput(tool, {
prompt: "say hello",
cron: "0 9 * * 1-5",
timezone: " America/New_York ",
provider: "codex",
});
expect(create).toHaveBeenCalledWith(
expect.objectContaining({
cadence: {
type: "cron",
expression: "0 9 * * 1-5",
timezone: "America/New_York",
},
}),
);
});
it("still rejects both real every and cron inputs", async () => {
const { agentManager, agentStorage } = createTestDeps();
const create = vi.fn();
@@ -2352,6 +2384,54 @@ describe("create_schedule MCP tool", () => {
expect(create).not.toHaveBeenCalled();
});
it("rejects create_schedule timezone without cron", async () => {
const { agentManager, agentStorage } = createTestDeps();
const create = vi.fn();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { create } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "create_schedule");
await expect(
invokeToolWithParsedInput(tool, {
prompt: "say hello",
every: "10m",
timezone: "America/New_York",
provider: "codex",
}),
).rejects.toThrow("timezone can only be used with cron");
expect(create).not.toHaveBeenCalled();
});
it.each(["", " "])("rejects create_schedule blank timezone %#", async (timezone) => {
const { agentManager, agentStorage } = createTestDeps();
const create = vi.fn();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { create } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "create_schedule");
await expect(
invokeToolWithParsedInput(tool, {
prompt: "say hello",
cron: "0 9 * * 1-5",
timezone,
provider: "codex",
}),
).rejects.toThrow();
expect(create).not.toHaveBeenCalled();
});
it.each([
{
label: "missing both cadence fields",
@@ -2458,6 +2538,35 @@ describe("update_schedule MCP tool", () => {
});
});
it("passes timezone through cron update_schedule input", async () => {
const { agentManager, agentStorage } = createTestDeps();
const stored = makeStoredSchedule();
const update = vi.fn(async (_input: UpdateScheduleInput) => stored);
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { update } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "update_schedule");
await invokeToolWithParsedInput(tool, {
id: "schedule-1",
cron: "0 9 * * 1-5",
timezone: "Europe/Zurich",
});
expect(update).toHaveBeenCalledWith({
id: "schedule-1",
cadence: {
type: "cron",
expression: "0 9 * * 1-5",
timezone: "Europe/Zurich",
},
});
});
it("accepts a blank cron field when updating every cadence", async () => {
const { agentManager, agentStorage } = createTestDeps();
const stored = makeStoredSchedule();
@@ -2542,6 +2651,52 @@ describe("update_schedule MCP tool", () => {
expect(update).not.toHaveBeenCalled();
});
it("rejects update_schedule timezone without cron", async () => {
const { agentManager, agentStorage } = createTestDeps();
const update = vi.fn();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { update } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "update_schedule");
await expect(
invokeToolWithParsedInput(tool, {
id: "schedule-1",
every: "10m",
timezone: "Europe/Zurich",
}),
).rejects.toThrow("timezone can only be used with cron");
expect(update).not.toHaveBeenCalled();
});
it.each(["", " "])("rejects update_schedule blank timezone %#", async (timezone) => {
const { agentManager, agentStorage } = createTestDeps();
const update = vi.fn();
const server = await createAgentMcpServer({
agentManager,
agentStorage,
providerSnapshotManager: createOpenCodeManager().manager,
scheduleService: { update } as unknown as ScheduleService,
logger,
});
const tool = registeredTool(server, "update_schedule");
await expect(
invokeToolWithParsedInput(tool, {
id: "schedule-1",
cron: "0 9 * * 1-5",
timezone,
}),
).rejects.toThrow();
expect(update).not.toHaveBeenCalled();
});
it("passes new-agent config and expiry updates", async () => {
const { agentManager, agentStorage } = createTestDeps();
const stored = makeStoredSchedule();
+40 -3
View File
@@ -333,6 +333,7 @@ interface ScheduleUpdateToolInput {
id: string;
every?: string;
cron?: string;
timezone?: string;
name?: string | null;
prompt?: string;
maxRuns?: number | null;
@@ -357,18 +358,30 @@ function normalizeScheduleCadenceArg(value: string | undefined): string | undefi
return trimmed;
}
function normalizeScheduleTimeZoneArg(value: string | undefined): string | undefined {
return normalizeScheduleCadenceArg(value);
}
function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleCadence | undefined {
const every = normalizeScheduleCadenceArg(input.every);
const cron = normalizeScheduleCadenceArg(input.cron);
const timeZone = normalizeScheduleTimeZoneArg(input.timezone);
if (every !== undefined && cron !== undefined) {
throw new Error("Specify at most one of every or cron");
}
if (timeZone !== undefined && cron === undefined) {
throw new Error("timezone can only be used with cron");
}
if (every !== undefined) {
return { type: "every", everyMs: parseDurationString(every) };
}
if (cron !== undefined) {
return { type: "cron", expression: cron };
return {
type: "cron",
expression: cron,
...(timeZone !== undefined ? { timezone: timeZone } : {}),
};
}
return undefined;
}
@@ -1583,6 +1596,14 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
prompt: z.string().trim().min(1, "prompt is required"),
every: z.string().optional(),
cron: z.string().optional(),
timezone: z
.string()
.trim()
.min(1)
.optional()
.describe(
"IANA time zone for cron cadence; requires cron. For example: America/New_York.",
),
name: z.string().optional(),
target: z.enum(["self", "new-agent"]).optional(),
provider: AgentProviderEnum.optional().describe(
@@ -1594,18 +1615,22 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
},
outputSchema: ScheduleSummarySchema.shape,
},
async ({ prompt, every, cron, name, target, provider, cwd, maxRuns, expiresIn }) => {
async ({ prompt, every, cron, timezone, name, target, provider, cwd, maxRuns, expiresIn }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const normalizedEvery = normalizeScheduleCadenceArg(every);
const normalizedCron = normalizeScheduleCadenceArg(cron);
const normalizedTimeZone = normalizeScheduleTimeZoneArg(timezone);
const cadenceCount =
Number(normalizedEvery !== undefined) + Number(normalizedCron !== undefined);
if (cadenceCount !== 1) {
throw new Error("Specify exactly one of every or cron");
}
if (normalizedTimeZone !== undefined && normalizedCron === undefined) {
throw new Error("timezone can only be used with cron");
}
const scheduleTarget =
target === "self"
@@ -1643,7 +1668,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
cadence:
normalizedEvery !== undefined
? { type: "every" as const, everyMs: parseDurationString(normalizedEvery) }
: { type: "cron" as const, expression: normalizedCron! },
: {
type: "cron" as const,
expression: normalizedCron!,
...(normalizedTimeZone !== undefined ? { timezone: normalizedTimeZone } : {}),
},
target: scheduleTarget,
...(name?.trim() ? { name: name.trim() } : {}),
...(maxRuns === undefined ? {} : { maxRuns }),
@@ -1792,6 +1821,14 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
id: z.string(),
every: z.string().optional().describe("New interval duration string (e.g. 5m, 1h)."),
cron: z.string().optional().describe("New cron expression."),
timezone: z
.string()
.trim()
.min(1)
.optional()
.describe(
"IANA time zone for cron cadence; requires cron. For example: America/New_York.",
),
name: z.string().nullable().optional().describe("New name (null to clear)."),
prompt: z.string().trim().min(1).optional().describe("New prompt text."),
maxRuns: z
@@ -20,9 +20,47 @@ describe("schedule cron cadence", () => {
expect(next.toISOString()).toBe("2026-01-05T09:15:00.000Z");
});
test("computes timezone cron matches at the requested wall-clock time", () => {
const winter = computeNextRunAt(
{ type: "cron", expression: "0 9 * * 1-5", timezone: "America/New_York" },
new Date("2026-01-05T13:59:30.000Z"),
);
const summer = computeNextRunAt(
{ type: "cron", expression: "0 9 * * 1-5", timezone: "America/New_York" },
new Date("2026-07-06T12:59:30.000Z"),
);
expect(winter.toISOString()).toBe("2026-01-05T14:00:00.000Z");
expect(summer.toISOString()).toBe("2026-07-06T13:00:00.000Z");
});
test("keeps repeated fall-back wall-clock matches distinct", () => {
const first = computeNextRunAt(
{ type: "cron", expression: "30 1 1 11 *", timezone: "America/New_York" },
new Date("2026-11-01T05:29:30.000Z"),
);
const second = computeNextRunAt(
{ type: "cron", expression: "30 1 1 11 *", timezone: "America/New_York" },
first,
);
expect(first.toISOString()).toBe("2026-11-01T05:30:00.000Z");
expect(second.toISOString()).toBe("2026-11-01T06:30:00.000Z");
});
test("rejects invalid cron expressions", () => {
expect(() => validateScheduleCadence({ type: "cron", expression: "not-a-valid-cron" })).toThrow(
"Cron expressions must have 5 fields",
);
});
test("rejects invalid cron time zones", () => {
expect(() =>
validateScheduleCadence({
type: "cron",
expression: "0 9 * * *",
timezone: "Not/AZone",
}),
).toThrow("Invalid cron time zone: Not/AZone");
});
});
+66 -5
View File
@@ -81,6 +81,14 @@ interface ParsedCronExpression {
dayOfWeek: CronFieldMatcher;
}
interface CronDateParts {
minute: number;
hour: number;
dayOfMonth: number;
month: number;
dayOfWeek: number;
}
function parseCronExpression(expression: string): ParsedCronExpression {
const parts = expression.trim().split(/\s+/);
if (parts.length !== 5) {
@@ -110,9 +118,65 @@ function startOfNextMinute(date: Date): Date {
);
}
function assertValidTimeZone(timeZone: string): void {
try {
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date(0));
} catch {
throw new Error(`Invalid cron time zone: ${timeZone}`);
}
}
function createCronDatePartsReader(timeZone: string | undefined): (date: Date) => CronDateParts {
if (timeZone === undefined) {
return (date: Date) => ({
minute: date.getUTCMinutes(),
hour: date.getUTCHours(),
dayOfMonth: date.getUTCDate(),
month: date.getUTCMonth() + 1,
dayOfWeek: date.getUTCDay(),
});
}
assertValidTimeZone(timeZone);
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
return (date: Date) => {
const values: Record<string, string> = {};
for (const part of formatter.formatToParts(date)) {
if (part.type !== "literal") {
values[part.type] = part.value;
}
}
const year = Number.parseInt(values.year, 10);
const month = Number.parseInt(values.month, 10);
const dayOfMonth = Number.parseInt(values.day, 10);
return {
minute: Number.parseInt(values.minute, 10),
hour: Number.parseInt(values.hour, 10),
dayOfMonth,
month,
dayOfWeek: new Date(Date.UTC(year, month - 1, dayOfMonth)).getUTCDay(),
};
};
}
export function validateScheduleCadence(cadence: ScheduleCadence): void {
if (cadence.type === "cron") {
parseCronExpression(cadence.expression);
if (cadence.timezone !== undefined) {
assertValidTimeZone(cadence.timezone);
}
}
}
@@ -122,15 +186,12 @@ export function computeNextRunAt(cadence: ScheduleCadence, after: Date): Date {
}
const cron = parseCronExpression(cadence.expression);
const readDateParts = createCronDatePartsReader(cadence.timezone);
const limit = 366 * 24 * 60;
let cursor = startOfNextMinute(after);
for (let index = 0; index < limit; index += 1) {
const minute = cursor.getUTCMinutes();
const hour = cursor.getUTCHours();
const dayOfMonth = cursor.getUTCDate();
const month = cursor.getUTCMonth() + 1;
const dayOfWeek = cursor.getUTCDay();
const { minute, hour, dayOfMonth, month, dayOfWeek } = readDateParts(cursor);
if (
cron.minute.matches(minute) &&