diff --git a/src/shared/automation-schedules.test.ts b/src/shared/automation-schedules.test.ts index 104af2eab..a9f8ac7de 100644 --- a/src/shared/automation-schedules.test.ts +++ b/src/shared/automation-schedules.test.ts @@ -81,6 +81,19 @@ describe('automation schedules', () => { expect(tryParseAutomationRrule('FREQ=WEEKLY;BYDAY=MO,NO;BYHOUR=10;BYMINUTE=15')).toBeNull() }) + it('rejects weekly RRULE schedules that cannot match any day', () => { + const rrule = 'FREQ=WEEKLY;BYHOUR=9;BYMINUTE=0' + + expect(isValidAutomationSchedule(rrule)).toBe(false) + expect(() => + nextAutomationOccurrenceAfter( + rrule, + new Date('2026-05-01T00:00:00').getTime(), + new Date('2026-05-02T00:00:00').getTime() + ) + ).toThrow('Invalid recurrence day.') + }) + it('formats invalid schedules with a safe fallback label', () => { expect(formatAutomationSchedule('FREQ=YEARLY')).toBe('Invalid schedule') }) diff --git a/src/shared/automation-schedules.ts b/src/shared/automation-schedules.ts index b83e414d3..47ede3a03 100644 --- a/src/shared/automation-schedules.ts +++ b/src/shared/automation-schedules.ts @@ -86,6 +86,13 @@ function parseRrule(rrule: string): ParsedRrule { throw new Error('Invalid recurrence minute.') } const byDay = (entries.get('BYDAY') ?? '').split(',').filter(Boolean) + if ( + freq === 'WEEKLY' && + (byDay.length === 0 || + byDay.some((day) => !DAY_CODES.includes(day as (typeof DAY_CODES)[number]))) + ) { + throw new Error('Invalid recurrence day.') + } return { kind: 'rrule', freq, byDay, byHour, byMinute } }