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
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user