diff --git a/apps/api/tests/broker/expand.test.ts b/apps/api/tests/broker/expand.test.ts index 1495fe3..8a05578 100644 --- a/apps/api/tests/broker/expand.test.ts +++ b/apps/api/tests/broker/expand.test.ts @@ -350,3 +350,144 @@ describe('expandOccurrences', () => { }); }); }); + +// ─── Phase 11 Plan 03 Task 3: reminderLeadMinutes on CalendarOccurrence ───────── +// D-06/D-10: reminderLeadMinutes is a series-level property — all occurrences of a +// recurring master inherit the master's lead. NULL-vs-0-vs-positive must survive +// through expansion. + +describe('expandOccurrences — reminderLeadMinutes propagation (Phase 11 Plan 03 Task 3)', () => { + // Helper: minimal VCALENDAR/VEVENT string for tests + function makeVevent(overrides: { + uid?: string; + allDay?: boolean; + reminderMinutes?: number | 'absolute' | 'none'; + rrule?: string; + }): string { + const uid = overrides.uid ?? 'test-uid@test'; + const lines: string[] = ['BEGIN:VCALENDAR', 'VERSION:2.0']; + + if (!overrides.allDay) { + lines.push('BEGIN:VEVENT'); + lines.push(`UID:${uid}`); + lines.push('SUMMARY:Test event'); + lines.push('DTSTART:20260615T140000Z'); + lines.push('DTEND:20260615T150000Z'); + } else { + lines.push('BEGIN:VEVENT'); + lines.push(`UID:${uid}`); + lines.push('SUMMARY:All-day test'); + lines.push('DTSTART;VALUE=DATE:20260615'); + lines.push('DTEND;VALUE=DATE:20260616'); + } + + if (overrides.rrule) { + lines.push(`RRULE:${overrides.rrule}`); + } + + if (overrides.reminderMinutes === 'absolute') { + lines.push('BEGIN:VALARM'); + lines.push('ACTION:DISPLAY'); + lines.push('DESCRIPTION:Reminder'); + lines.push('TRIGGER;VALUE=DATE-TIME:20260615T120000Z'); + lines.push('END:VALARM'); + } else if (overrides.reminderMinutes !== 'none' && overrides.reminderMinutes !== undefined) { + lines.push('BEGIN:VALARM'); + lines.push('ACTION:DISPLAY'); + lines.push('DESCRIPTION:Reminder'); + lines.push(`TRIGGER:-PT${overrides.reminderMinutes}M`); + lines.push('END:VALARM'); + } + + lines.push('END:VEVENT'); + lines.push('END:VCALENDAR'); + return lines.join('\r\n'); + } + + const WINDOW_START = new Date('2026-06-01T00:00:00Z'); + const WINDOW_END = new Date('2026-07-01T00:00:00Z'); + + // Non-recurring event with reminderLeadMinutes=30 → occurrence carries 30 + it('non-recurring event: occurrence carries reminderLeadMinutes from master (30 minutes)', () => { + const raw = makeVevent({ reminderMinutes: 30 }); + + const occurrences = expandOccurrences( + raw, + WINDOW_START, + WINDOW_END, + 1, + 'My Calendar', + 1, + null, + '#4A90D9', + false, + ); + + expect(occurrences).toHaveLength(1); + expect(occurrences[0].reminderLeadMinutes).toBe(30); + }); + + // NULL-vs-0: master with 0-minute all-day lead → occurrence carries 0, not null + it('non-recurring all-day event: occurrence carries reminderLeadMinutes=0 (same-day, D-06 NULL-vs-0)', () => { + // Use a 0-minute trigger (same-day all-day) + const raw = makeVevent({ allDay: true, reminderMinutes: 0 }); + + const occurrences = expandOccurrences( + raw, + new Date('2026-06-01T00:00:00Z'), + new Date('2026-07-01T00:00:00Z'), + 1, + 'My Calendar', + 1, + null, + '#4A90D9', + false, + ); + + expect(occurrences).toHaveLength(1); + // 0-minute all-day trigger → preset 0 → reminderLeadMinutes=0 (not null) + expect(occurrences[0].reminderLeadMinutes).toBe(0); + }); + + // NULL: no VALARM in master → occurrence carries null + it('non-recurring event with no VALARM: occurrence carries reminderLeadMinutes=null', () => { + const raw = makeVevent({ reminderMinutes: 'none' }); + + const occurrences = expandOccurrences( + raw, + WINDOW_START, + WINDOW_END, + 1, + 'My Calendar', + 1, + null, + '#4A90D9', + false, + ); + + expect(occurrences).toHaveLength(1); + expect(occurrences[0].reminderLeadMinutes).toBeNull(); + }); + + // D-10 series-level: recurring event with reminderLeadMinutes=60 → all occurrences carry 60 + it('D-10 series-level: all recurring occurrences inherit the master reminderLeadMinutes=60', () => { + const raw = makeVevent({ reminderMinutes: 60, rrule: 'FREQ=WEEKLY;COUNT=3' }); + + const occurrences = expandOccurrences( + raw, + WINDOW_START, + WINDOW_END, + 1, + 'My Calendar', + 1, + null, + '#4A90D9', + false, + ); + + expect(occurrences.length).toBeGreaterThan(0); + for (const occ of occurrences) { + expect(occ.reminderLeadMinutes).toBe(60); + } + }); +});