diff --git a/src/lib/cron.test.ts b/src/lib/cron.test.ts index 247a6b4..af7ab20 100644 --- a/src/lib/cron.test.ts +++ b/src/lib/cron.test.ts @@ -107,6 +107,15 @@ describe('describeCron', () => { } }); + it('returns the raw expression when the month is restricted', () => { + // Every phrasing describeCron can produce names a cadence, and none of them + // can carry "only in these months". Saying "daily at 03:00" for a June-only + // cron contradicts the ~3 times/month printed next to it by the advisory. + for (const cron of ['0 3 * 6 *', '0 3 * 1,7 *', '0 3 * */6 *', '0 3 1 3 *', '0 3 * 6 1']) { + expect(describeCron(cron), cron).toBe(cron); + } + }); + it('returns the raw expression when the field count is not 5', () => { // A 6-field Quartz cron shifts every index, so reading f[1] as the hour // would describe a time the schedule never runs at. @@ -145,6 +154,16 @@ describe('formatScheduleFrequencyAdvisory', () => { expect(out).not.toContain('~0 time'); }); + it('does not call a month-restricted schedule daily', () => { + // Regression: describeCron used to skip the month field entirely, so this + // rendered as "~3 time(s)/month (daily at 03:00)" -- two claims that cannot + // both be true, in one sentence. + const out = formatScheduleFrequencyAdvisory('0 3 * 6 *'); + expect(out).toContain('~3 time(s)/month'); + expect(out).not.toContain('daily'); + expect(out).toContain('0 3 * 6 *'); + }); + it('still advises, without a frequency, for an expression it cannot read', () => { const out = formatScheduleFrequencyAdvisory('0 0 3 * * *'); expect(out).toContain('0 0 3 * * *'); diff --git a/src/lib/cron.ts b/src/lib/cron.ts index ba11c6f..46f91b4 100644 --- a/src/lib/cron.ts +++ b/src/lib/cron.ts @@ -145,9 +145,15 @@ export function runsPerMonth(cron: string): number | null { export function describeCron(cron: string): string { const f = fields(cron); if (f.length !== FIELD_COUNT) return cron.trim(); - const [minute, hour, dayOfMonth, , dayOfWeek] = f; + const [minute, hour, dayOfMonth, month, dayOfWeek] = f; if (!isPinned(minute) || !isPinned(hour)) return cron.trim(); + // A restricted month makes every phrasing below wrong, because none of them + // can say "except in the months this cron skips": `0 3 * 6 *` is not "daily + // at 03:00", it is daily *during June*. runsPerMonth already divides by the + // month count, so describing it as daily contradicts the frequency printed + // beside it in the same sentence. + if (!isWildcard(month)) return cron.trim(); const at = `${hour!.padStart(2, '0')}:${minute!.padStart(2, '0')}`; if (isWildcard(dayOfMonth) && isWildcard(dayOfWeek)) return `daily at ${at}`;