From cdca93094abe47759a6b786c0751905de40d4aa2 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 22:17:51 -0400 Subject: [PATCH] test(11-03): add failing tests for sync.ts reminderLeadMinutes upsert from VALARM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - preset TRIGGER:-PT30M → reminderLeadMinutes=30 - no VALARM → reminderLeadMinutes=null - absolute DATE-TIME trigger → null (custom kind, D-07/NOTIF-05) - two VALARMs → null (multiple alarms not resolvable to single lead) - onDuplicateKeyUpdate set also carries reminderLeadMinutes (upsert keeps column current) --- apps/api/tests/broker/sync.test.ts | 204 +++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/apps/api/tests/broker/sync.test.ts b/apps/api/tests/broker/sync.test.ts index 10b9858..bc3e862 100644 --- a/apps/api/tests/broker/sync.test.ts +++ b/apps/api/tests/broker/sync.test.ts @@ -429,3 +429,207 @@ describe('syncCalendar', () => { expect(collectedChanges[1]).toMatchObject({ uid: 'uid-to-delete-2', operation: 'delete' }); }); }); + +// ─── Phase 11 Plan 03 Task 2: reminderLeadMinutes derived from VALARM on sync ── +// CAL-13: sync.ts must derive reminderLeadMinutes from the native VALARM and write it +// to the DB so the scheduler has ground truth for native-client alarms (D-07/NOTIF-05). + +describe('syncCalendar — reminderLeadMinutes from VALARM (Phase 11 Plan 03 Task 2)', () => { + const MOCK_DAV_CAL = { + url: 'https://caldav.fastmail.com/dav/calendars/user/test@fm.com/Default/', + displayName: 'Test Calendar', + ctag: 'ctag-v1', + syncToken: null, + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockOnDuplicateKeyUpdate.mockResolvedValue([{ insertId: 1 }]); + mockValues.mockReturnValue({ onDuplicateKeyUpdate: mockOnDuplicateKeyUpdate }); + mockInsert.mockReturnValue({ values: mockValues }); + mockLimit.mockResolvedValue([{ id: 42 }]); + mockWhere.mockReturnValue({ limit: mockLimit }); + mockFrom.mockReturnValue({ where: mockWhere }); + mockSelect.mockReturnValue({ from: mockFrom }); + mockDeleteWhere.mockResolvedValue([]); + mockDelete.mockReturnValue({ where: mockDeleteWhere }); + }); + + // A single preset TRIGGER:-PT30M → reminderLeadMinutes=30 + it('writes reminderLeadMinutes=30 when VCALENDAR has a single TRIGGER:-PT30M VALARM', async () => { + const { syncCalendar } = await import('../../src/broker/sync.js'); + + const rawVeventWithValarm = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:uid-with-valarm@test', + 'SUMMARY:Meeting with reminder', + 'DTSTART:20260615T140000Z', + 'DTEND:20260615T150000Z', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Reminder', + 'TRIGGER:-PT30M', + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: rawVeventWithValarm, etag: '"etag-valarm"', url: '/cal/valarm.ics' }, + ]), + }; + + await syncCalendar(mockClient as never, MOCK_DAV_CAL as never, 1); + + const eventValuesArg = mockValues.mock.calls[1][0]; + expect(eventValuesArg.reminderLeadMinutes).toBe(30); + }); + + // No VALARM → reminderLeadMinutes=null + it('writes reminderLeadMinutes=null when VCALENDAR has no VALARM', async () => { + const { syncCalendar } = await import('../../src/broker/sync.js'); + + const rawVeventNoValarm = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:uid-no-valarm@test', + 'SUMMARY:Event without reminder', + 'DTSTART:20260615T140000Z', + 'DTEND:20260615T150000Z', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: rawVeventNoValarm, etag: '"etag-no-valarm"', url: '/cal/no-valarm.ics' }, + ]), + }; + + await syncCalendar(mockClient as never, MOCK_DAV_CAL as never, 1); + + const eventValuesArg = mockValues.mock.calls[1][0]; + expect(eventValuesArg.reminderLeadMinutes).toBeNull(); + }); + + // Absolute DATE-TIME trigger → reminderLeadMinutes=null (custom kind, D-07/NOTIF-05) + it('writes reminderLeadMinutes=null when VALARM has absolute DATE-TIME trigger (custom → null)', async () => { + const { syncCalendar } = await import('../../src/broker/sync.js'); + + const rawVeventAbsoluteValarm = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:uid-absolute-valarm@test', + 'SUMMARY:Event with absolute VALARM', + 'DTSTART:20260615T140000Z', + 'DTEND:20260615T150000Z', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Reminder', + 'TRIGGER;VALUE=DATE-TIME:20260615T120000Z', + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { + data: rawVeventAbsoluteValarm, + etag: '"etag-abs"', + url: '/cal/abs.ics', + }, + ]), + }; + + await syncCalendar(mockClient as never, MOCK_DAV_CAL as never, 1); + + const eventValuesArg = mockValues.mock.calls[1][0]; + // Absolute DATE-TIME trigger → classifyValarms returns 'custom' → null + expect(eventValuesArg.reminderLeadMinutes).toBeNull(); + }); + + // Two VALARMs → reminderLeadMinutes=null (custom, multiple alarms not resolvable to one lead) + it('writes reminderLeadMinutes=null when VCALENDAR has two VALARMs (multiple → custom → null)', async () => { + const { syncCalendar } = await import('../../src/broker/sync.js'); + + const rawVeventTwoValarms = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:uid-two-valarms@test', + 'SUMMARY:Event with two alarms', + 'DTSTART:20260615T140000Z', + 'DTEND:20260615T150000Z', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:First Reminder', + 'TRIGGER:-PT30M', + 'END:VALARM', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Second Reminder', + 'TRIGGER:-PT15M', + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { + data: rawVeventTwoValarms, + etag: '"etag-two"', + url: '/cal/two.ics', + }, + ]), + }; + + await syncCalendar(mockClient as never, MOCK_DAV_CAL as never, 1); + + const eventValuesArg = mockValues.mock.calls[1][0]; + // Multiple VALARMs → classifyValarms returns 'custom' → null + expect(eventValuesArg.reminderLeadMinutes).toBeNull(); + }); + + // Ensure the onDuplicateKeyUpdate ALSO sets reminderLeadMinutes (upsert column must be current) + it('sets reminderLeadMinutes in onDuplicateKeyUpdate set (re-sync keeps column current)', async () => { + const { syncCalendar } = await import('../../src/broker/sync.js'); + + const rawVeventWithValarm = [ + 'BEGIN:VCALENDAR', + 'VERSION:2.0', + 'BEGIN:VEVENT', + 'UID:uid-upsert@test', + 'SUMMARY:Recurring meeting', + 'DTSTART:20260615T100000Z', + 'DTEND:20260615T110000Z', + 'BEGIN:VALARM', + 'ACTION:DISPLAY', + 'DESCRIPTION:Reminder', + 'TRIGGER:-PT15M', + 'END:VALARM', + 'END:VEVENT', + 'END:VCALENDAR', + ].join('\r\n'); + + const mockClient = { + fetchCalendarObjects: vi.fn().mockResolvedValue([ + { data: rawVeventWithValarm, etag: '"etag-upsert"', url: '/cal/upsert.ics' }, + ]), + }; + + await syncCalendar(mockClient as never, MOCK_DAV_CAL as never, 1); + + // The onDuplicateKeyUpdate `set` object must also contain reminderLeadMinutes + const upsertSetArg = mockOnDuplicateKeyUpdate.mock.calls[1]?.[0] as { + set?: Record; + }; + expect(upsertSetArg?.set).toHaveProperty('reminderLeadMinutes', 15); + }); +});