diff --git a/assistant/src/__tests__/schedule-store.test.ts b/assistant/src/__tests__/schedule-store.test.ts index 9038d85822e..deea1dc0557 100644 --- a/assistant/src/__tests__/schedule-store.test.ts +++ b/assistant/src/__tests__/schedule-store.test.ts @@ -435,8 +435,12 @@ describe('claimDueSchedules', () => { }); test('claims exhausted RRULE schedule and disables it', () => { - // COUNT=1 means only one occurrence — the DTSTART itself - const rrule = 'DTSTART:20250101T000000Z\nRRULE:FREQ=DAILY;COUNT=1'; + // COUNT=1 means only one occurrence — the DTSTART itself. + // Use a far-future DTSTART so createSchedule succeeds (it validates that at + // least one run exists from now). We then force next_run_at into the past + // via SQL to simulate the schedule becoming due after its only occurrence. + const futureYear = new Date().getFullYear() + 5; + const rrule = `DTSTART:${futureYear}0101T000000Z\nRRULE:FREQ=DAILY;COUNT=1`; const job = createSchedule({ name: 'Finite RRULE', cronExpression: rrule, diff --git a/assistant/src/schedule/schedule-store.ts b/assistant/src/schedule/schedule-store.ts index 25eb680430b..dafb5532bf2 100644 --- a/assistant/src/schedule/schedule-store.ts +++ b/assistant/src/schedule/schedule-store.ts @@ -216,7 +216,12 @@ export function claimDueSchedules(now: number): ScheduleJob[] { expression: row.cronExpression, timezone: row.timezone, }); - } catch { + } catch (err) { + // Only treat "no upcoming runs" as exhaustion — rethrow other failures + // (e.g. invalid RRULE lines, unsupported syntax) so they surface instead + // of silently disabling a schedule that has a configuration bug. + const msg = err instanceof Error ? err.message : String(err); + if (!msg.includes('no upcoming runs')) throw err; // Finite schedule with no future runs — still claim the current due // run but disable the schedule so it doesn't fire again. newNextRunAt = null;