diff --git a/src/shared/automation-schedules.test.ts b/src/shared/automation-schedules.test.ts index 5ff4ebec73b..0a867c52e4b 100644 --- a/src/shared/automation-schedules.test.ts +++ b/src/shared/automation-schedules.test.ts @@ -200,6 +200,34 @@ describe('automation schedules', () => { expect(isValidAutomationSchedule('0 9 * * 1-7')).toBe(true) }) + it('runs a bare start value with a step through the end of its field', () => { + // Why: `5/15` is the shorthand every cron editor and Quartz read as + // "from 5, every 15" — 5,20,35,50. Dropping the step silently turned it + // into a single value, so the automation ran hourly instead. + const anchor = new Date('2026-05-01T00:00:00').getTime() + const runs: number[] = [] + let cursor = anchor + for (let index = 0; index < 4; index += 1) { + const next = nextAutomationOccurrenceAfter('5/15 * * * *', anchor, cursor) + if (next === null) { + break + } + runs.push(new Date(next).getMinutes()) + cursor = next + } + + expect(runs).toEqual([5, 20, 35, 50]) + // A bare value with no step still means exactly that value. + expect(new Date(nextAutomationOccurrenceAfter('5 * * * *', anchor, anchor)!).getMinutes()).toBe( + 5 + ) + // `1/2` in day-of-week is Mon/Wed/Fri/Sun, matching `1-7/2`. + expect(isValidAutomationSchedule('0 9 * * 1/2')).toBe(true) + expect(nextAutomationOccurrenceAfter('0 9 * * 1/2', anchor, anchor)).toBe( + nextAutomationOccurrenceAfter('0 9 * * 1-7/2', anchor, anchor) + ) + }) + it('rejects cron fields with malformed separators', () => { expect(isValidAutomationSchedule('*/15/2 9 * * *')).toBe(false) expect(isValidAutomationSchedule('0 9 1--5 * *')).toBe(false) diff --git a/src/shared/automation-schedules.ts b/src/shared/automation-schedules.ts index d8a93499287..e4331361501 100644 --- a/src/shared/automation-schedules.ts +++ b/src/shared/automation-schedules.ts @@ -150,7 +150,11 @@ function parseCronField(args: { end = parseCronNumber(endPart, args.names ?? null, args.field) } else { start = parseCronNumber(rangePart, args.names ?? null, args.field) - end = start + // Why: `5/15` is the shorthand for `5-/15` that Quartz and every cron + // editor read as "from 5, every 15". Ending at `start` kept the value and + // dropped the step, which no cron dialect does — the automation then ran + // once per field cycle instead of on the interval the user wrote. + end = stepPart === undefined ? start : args.max } const normalizedStart = args.normalize?.(start) ?? start