From 79f6871167fba517218883c787f597bcfc66aa12 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 22:14:42 -0400 Subject: [PATCH] test(11-03): add failing tests for reminderLeadMinutes VALARM wiring (CAL-13/CAL-14) - CAL-14 preserve: UPDATE with no reminderLeadMinutes preserves VALARM from rawVevent - CAL-13 timed: CREATE with reminderLeadMinutes=15 emits TRIGGER:-PT15M - CAL-13 clear: UPDATE with reminderLeadMinutes=null emits no VALARM (passes trivially) - CAL-13 all-day: CREATE with allDay=true and reminderLeadMinutes=1440 emits VALUE=DATE-TIME --- apps/api/tests/broker/outboxWorker.test.ts | 154 +++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/apps/api/tests/broker/outboxWorker.test.ts b/apps/api/tests/broker/outboxWorker.test.ts index 7223409..e3755c2 100644 --- a/apps/api/tests/broker/outboxWorker.test.ts +++ b/apps/api/tests/broker/outboxWorker.test.ts @@ -864,3 +864,157 @@ describe('scheduleOutboxDrain — trigger wiring (D-09)', () => { } }); }); + +// ─── Phase 11 Plan 03: reminderLeadMinutes schema + VALARM wiring ─────────────── +// CAL-13: reminderLeadMinutes round-trips end-to-end through outbox payload → +// buildVeventString → emitted ICS. +// CAL-14: UPDATE row with no reminderLeadMinutes in payload preserves existing +// VALARM verbatim from rawVevent (mirrors WR-01 _preservedRrule pattern). + +describe('runOutboxDrain — reminderLeadMinutes VALARM wiring (CAL-13/CAL-14, Phase 11 Plan 03)', () => { + beforeEach(() => { + vi.resetAllMocks(); + mockPendingRows = []; + wireMockChain(); + }); + + // CAL-14: UPDATE row with NO reminderLeadMinutes field, but rawVevent has a VALARM → + // emitted ICS must still contain BEGIN:VALARM (preserve path, mirrors _preservedRrule WR-01). + it('CAL-14 preserve: UPDATE with no reminderLeadMinutes field preserves existing VALARM from rawVevent', async () => { + const { updateCalendarEvent } = await import('../../src/broker/write.js'); + let capturedIcsString: unknown = null; + vi.mocked(updateCalendarEvent).mockImplementation(async (_client, _url, icsString, _etag) => { + capturedIcsString = icsString; + return makeResponse(204); + }); + + // rawVevent that already has a VALARM (TRIGGER:-PT30M) + const rawVeventWithValarm = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:test-uid@familysync', + 'SUMMARY:Team meeting', + 'DTSTART:20260610T120000Z', + 'DTEND:20260610T130000Z', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Reminder', + 'TRIGGER:-PT30M', + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + // Payload has NO reminderLeadMinutes key (absent = no-change, D-08) + const updatePayload = JSON.stringify({ + title: 'Team meeting', + allDay: false, + start: '2026-06-10T12:00:00.000Z', + end: '2026-06-10T13:00:00.000Z', + }); + + mockPendingRows = [ + makeRow({ + operation: 'update', + calendarObjectUrl: 'https://caldav.fastmail.com/dav/calendars/user/test/uid.ics', + payload: updatePayload, + }), + ]; + + // Simulate freshEtagRows returning rawVevent that has a VALARM + mockWhereCalEvents.mockResolvedValue([{ etag: '"fresh"', rawVevent: rawVeventWithValarm }]); + + await runOutboxDrain(); + + expect(typeof capturedIcsString).toBe('string'); + // The emitted ICS must contain the preserved VALARM + expect(capturedIcsString as string).toContain('BEGIN:VALARM'); + expect(capturedIcsString as string).toContain('TRIGGER:-PT30M'); + }); + + // CAL-13: CREATE row with reminderLeadMinutes=15 → emitted ICS contains TRIGGER:-PT15M + it('CAL-13 timed: CREATE row with reminderLeadMinutes=15 emits TRIGGER:-PT15M', async () => { + const { createCalendarEvent } = await import('../../src/broker/write.js'); + let capturedIcsString: unknown = null; + vi.mocked(createCalendarEvent).mockImplementation(async (_client, _cal, _uid, icsString) => { + capturedIcsString = icsString; + return makeResponse(201); + }); + + const payload = JSON.stringify({ + title: 'Doctor appointment', + allDay: false, + start: '2026-06-15T14:00:00.000Z', + end: '2026-06-15T15:00:00.000Z', + reminderLeadMinutes: 15, + }); + + mockPendingRows = [makeRow({ payload })]; + + await runOutboxDrain(); + + expect(typeof capturedIcsString).toBe('string'); + expect(capturedIcsString as string).toContain('BEGIN:VALARM'); + expect(capturedIcsString as string).toContain('TRIGGER:-PT15M'); + }); + + // CAL-13 clear: UPDATE row with reminderLeadMinutes=null → emitted ICS has no VALARM + it('CAL-13 clear: UPDATE row with reminderLeadMinutes=null emits no VALARM (explicit clear)', async () => { + const { updateCalendarEvent } = await import('../../src/broker/write.js'); + let capturedIcsString: unknown = null; + vi.mocked(updateCalendarEvent).mockImplementation(async (_client, _url, icsString, _etag) => { + capturedIcsString = icsString; + return makeResponse(204); + }); + + const updatePayload = JSON.stringify({ + title: 'No reminder event', + allDay: false, + start: '2026-06-15T14:00:00.000Z', + end: '2026-06-15T15:00:00.000Z', + reminderLeadMinutes: null, + }); + + mockPendingRows = [ + makeRow({ + operation: 'update', + calendarObjectUrl: 'https://caldav.fastmail.com/dav/calendars/user/test/uid.ics', + payload: updatePayload, + }), + ]; + + await runOutboxDrain(); + + expect(typeof capturedIcsString).toBe('string'); + expect(capturedIcsString as string).not.toContain('BEGIN:VALARM'); + }); + + // CAL-13 all-day: CREATE row with allDay=true and reminderLeadMinutes=1440 → + // emitted ICS contains VALUE=DATE-TIME absolute trigger (not DURATION trigger). + it('CAL-13 all-day: CREATE row with allDay=true and reminderLeadMinutes=1440 emits VALUE=DATE-TIME trigger', async () => { + const { createCalendarEvent } = await import('../../src/broker/write.js'); + let capturedIcsString: unknown = null; + vi.mocked(createCalendarEvent).mockImplementation(async (_client, _cal, _uid, icsString) => { + capturedIcsString = icsString; + return makeResponse(201); + }); + + const payload = JSON.stringify({ + title: 'Birthday party', + allDay: true, + start: '2026-06-20', + end: '2026-06-20', + reminderLeadMinutes: 1440, // 1 day before = leadDays = 1440/1440 = 1 + }); + + mockPendingRows = [makeRow({ payload })]; + + await runOutboxDrain(); + + expect(typeof capturedIcsString).toBe('string'); + expect(capturedIcsString as string).toContain('BEGIN:VALARM'); + // Must use VALUE=DATE-TIME absolute trigger for all-day (not DURATION) + expect(capturedIcsString as string).toContain('VALUE=DATE-TIME'); + }); +});