merge(phase-11): wave 2 plan 11-03 backend plumbing
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, unknown>;
|
||||
};
|
||||
expect(upsertSetArg?.set).toHaveProperty('reminderLeadMinutes', 15);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user