test(11-03): add failing tests for sync.ts reminderLeadMinutes upsert from VALARM
- 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)
This commit is contained in:
@@ -429,3 +429,207 @@ describe('syncCalendar', () => {
|
|||||||
expect(collectedChanges[1]).toMatchObject({ uid: 'uid-to-delete-2', operation: 'delete' });
|
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<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(upsertSetArg?.set).toHaveProperty('reminderLeadMinutes', 15);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user