Phase 11: Per-Event Reminders (CAL-13/14, NOTIF-04/05/06) #19
@@ -16,6 +16,12 @@
|
|||||||
* - D-16: empty push_subscriptions -> zero sends / no crash
|
* - D-16: empty push_subscriptions -> zero sends / no crash
|
||||||
* - T-05-19: per-subscription error isolation
|
* - T-05-19: per-subscription error isolation
|
||||||
*
|
*
|
||||||
|
* NEW (Plan 11-02):
|
||||||
|
* - NOTIF-04: fires at T-reminderLeadMinutes (variable per-event lead), not fixed 16-min window
|
||||||
|
* - NOTIF-05: NULL lead → no push; timed 0-lead → no push; personal events DO dispatch
|
||||||
|
* - NOTIF-06: uid:dtstartMs compound dedup; rescheduled dtstart re-fires; all-day 9 AM branch
|
||||||
|
* - D-09: humanized push body (humanizeLeadMinutes)
|
||||||
|
*
|
||||||
* Run: pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts
|
* Run: pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -50,7 +56,11 @@ function makeSelectMock(rows: unknown[]) {
|
|||||||
function makeEventRow(overrides: {
|
function makeEventRow(overrides: {
|
||||||
uid?: string;
|
uid?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
dtstartUtc: Date;
|
dtstartUtc?: Date | null;
|
||||||
|
dtstartDate?: string | null;
|
||||||
|
allDay?: boolean;
|
||||||
|
isShared?: boolean;
|
||||||
|
reminderLeadMinutes?: number | null;
|
||||||
subId?: number | null;
|
subId?: number | null;
|
||||||
subUserId?: number | null;
|
subUserId?: number | null;
|
||||||
subEndpoint?: string;
|
subEndpoint?: string;
|
||||||
@@ -60,9 +70,11 @@ function makeEventRow(overrides: {
|
|||||||
return {
|
return {
|
||||||
uid: overrides.uid ?? 'test-uid-1',
|
uid: overrides.uid ?? 'test-uid-1',
|
||||||
title: overrides.title ?? 'Test Event',
|
title: overrides.title ?? 'Test Event',
|
||||||
dtstartUtc: overrides.dtstartUtc,
|
dtstartUtc: overrides.dtstartUtc !== undefined ? overrides.dtstartUtc : null,
|
||||||
allDay: false,
|
dtstartDate: overrides.dtstartDate !== undefined ? overrides.dtstartDate : null,
|
||||||
isShared: true,
|
allDay: overrides.allDay ?? false,
|
||||||
|
isShared: overrides.isShared ?? true,
|
||||||
|
reminderLeadMinutes: overrides.reminderLeadMinutes !== undefined ? overrides.reminderLeadMinutes : 15,
|
||||||
subId: overrides.subId ?? 1,
|
subId: overrides.subId ?? 1,
|
||||||
subUserId: overrides.subUserId ?? 1,
|
subUserId: overrides.subUserId ?? 1,
|
||||||
subEndpoint: overrides.subEndpoint ?? 'https://push.example.com/1',
|
subEndpoint: overrides.subEndpoint ?? 'https://push.example.com/1',
|
||||||
@@ -71,9 +83,9 @@ function makeEventRow(overrides: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Filtering tests (D-05 / D-07) ────────────────────────────────────────────
|
// ── Filtering tests (NOTIF-04/05: variable lead, NULL-vs-0, personal calendar) ────────
|
||||||
|
|
||||||
describe('reminderScheduler — shared+timed event filtering (D-05/D-07)', () => {
|
describe('reminderScheduler — variable-lead, NULL-vs-0, personal calendar (NOTIF-04/05)', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -84,7 +96,9 @@ describe('reminderScheduler — shared+timed event filtering (D-05/D-07)', () =>
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not dispatch reminders for all-day events (D-07)', async () => {
|
it('NOTIF-04: dispatches a timed event when now is inside the lead-driven fire window (30-min lead)', async () => {
|
||||||
|
// Fire time = dtstartUtc - 30min. now = dtstartUtc - 30min (exactly at fire time).
|
||||||
|
// The event MUST dispatch. It would NOT dispatch under the old fixed 16-min window.
|
||||||
const now = new Date('2026-06-15T10:00:00Z');
|
const now = new Date('2026-06-15T10:00:00Z');
|
||||||
vi.setSystemTime(now);
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
@@ -92,15 +106,38 @@ describe('reminderScheduler — shared+timed event filtering (D-05/D-07)', () =>
|
|||||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
// allDay=true events are excluded by the SQL WHERE; simulate by returning empty rows
|
const dtstartUtc = new Date('2026-06-15T10:30:00Z'); // 30 min from now
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
const row = makeEventRow({
|
||||||
|
uid: 'notif04-30min-uid',
|
||||||
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 30,
|
||||||
|
isShared: true,
|
||||||
|
});
|
||||||
|
|
||||||
await runReminderCheck();
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
|
||||||
|
expect(vi.mocked(dispatchPush)).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NOTIF-04: does NOT dispatch a 30-min lead event when now is 5 min before start (fire time already passed)', async () => {
|
||||||
|
// Fire time = dtstartUtc - 30min = 09:30. now = 10:25 (event in 5 min, past fire time).
|
||||||
|
// SQL should not return this row (fire time check). Simulate with empty mock.
|
||||||
|
const now = new Date('2026-06-15T10:25:00Z');
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
|
const { db } = await import('../../src/db/client.js');
|
||||||
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
|
// Simulate: event would have been in old 16-min window but already past its fire time
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
|
||||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not dispatch reminders for non-shared (personal) calendar events (D-05)', async () => {
|
it('NOTIF-05: a personal (isShared=false) timed event with non-null lead DOES dispatch (restriction dropped)', async () => {
|
||||||
const now = new Date('2026-06-15T10:00:00Z');
|
const now = new Date('2026-06-15T10:00:00Z');
|
||||||
vi.setSystemTime(now);
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
@@ -108,10 +145,55 @@ describe('reminderScheduler — shared+timed event filtering (D-05/D-07)', () =>
|
|||||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
// isShared=false events are excluded by the SQL WHERE; simulate by returning empty rows
|
const dtstartUtc = new Date('2026-06-15T10:15:00Z');
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
const row = makeEventRow({
|
||||||
|
uid: 'personal-event-uid',
|
||||||
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
|
isShared: false, // personal calendar — must still dispatch
|
||||||
|
});
|
||||||
|
|
||||||
await runReminderCheck();
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
|
||||||
|
// Personal event must dispatch (isShared restriction dropped per NOTIF-05)
|
||||||
|
expect(vi.mocked(dispatchPush)).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NOTIF-05: an event with reminderLeadMinutes=NULL produces zero dispatches', async () => {
|
||||||
|
const now = new Date('2026-06-15T10:00:00Z');
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
|
const { db } = await import('../../src/db/client.js');
|
||||||
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
|
// NULL lead events are excluded by SQL WHERE (reminder_lead_minutes IS NOT NULL)
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
|
||||||
|
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('NOTIF-05: a timed event with reminderLeadMinutes=0 produces zero dispatches (D-06: 0 on timed = None)', async () => {
|
||||||
|
const now = new Date('2026-06-15T10:00:00Z');
|
||||||
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
|
const { db } = await import('../../src/db/client.js');
|
||||||
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
|
const dtstartUtc = new Date('2026-06-15T10:01:00Z');
|
||||||
|
const row = makeEventRow({
|
||||||
|
uid: 'timed-zero-uid',
|
||||||
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 0, // D-06: 0 on timed = None (skip)
|
||||||
|
allDay: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Even if the query returned a timed-0 event, JS must skip it
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
|
||||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -175,13 +257,14 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('SINGLE-FIRE: dispatches exactly once across three consecutive ticks while event is in window', async () => {
|
it('SINGLE-FIRE (uid:dtstartMs): dispatches exactly once across three consecutive ticks while event is in window', async () => {
|
||||||
|
// NOTIF-06: dedup key is uid:dtstartMs — same compound key fires once across 3 ticks
|
||||||
const { db } = await import('../../src/db/client.js');
|
const { db } = await import('../../src/db/client.js');
|
||||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
const t0 = new Date('2026-06-15T10:00:00Z');
|
const t0 = new Date('2026-06-15T10:00:00Z');
|
||||||
// Event is 15 min out from t0; still in (now, now+16min] at t0+1min (14 min out) and t0+2min (13 min out)
|
// Event is 15 min out from t0. Fire window (now-60s, now] catches it at t0.
|
||||||
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
||||||
|
|
||||||
const sub = {
|
const sub = {
|
||||||
@@ -196,7 +279,7 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
|||||||
return makeEventRow({
|
return makeEventRow({
|
||||||
uid: 'single-fire-uid',
|
uid: 'single-fire-uid',
|
||||||
dtstartUtc: eventDtstart,
|
dtstartUtc: eventDtstart,
|
||||||
...sub,
|
reminderLeadMinutes: 15,
|
||||||
subId: sub.id,
|
subId: sub.id,
|
||||||
subUserId: sub.userId,
|
subUserId: sub.userId,
|
||||||
subEndpoint: sub.endpoint,
|
subEndpoint: sub.endpoint,
|
||||||
@@ -205,54 +288,92 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick at t0 (event 15 min out)
|
// Tick at t0 — fire time is exactly now (dtstartUtc - 15min = t0); dispatches once
|
||||||
vi.setSystemTime(t0);
|
vi.setSystemTime(t0);
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||||
await runReminderCheck(t0);
|
await runReminderCheck(t0);
|
||||||
|
|
||||||
// Tick at t0+1min (event 14 min out — still in window, same uid)
|
// Tick at t0+1min — same uid:dtstartMs already in sentReminders; must NOT re-dispatch
|
||||||
const t1 = new Date(t0.getTime() + 60 * 1000);
|
const t1 = new Date(t0.getTime() + 60 * 1000);
|
||||||
vi.setSystemTime(t1);
|
vi.setSystemTime(t1);
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||||
await runReminderCheck(t1);
|
await runReminderCheck(t1);
|
||||||
|
|
||||||
// Tick at t0+2min (event 13 min out — still in window, same uid)
|
// Tick at t0+2min — still deduped
|
||||||
const t2 = new Date(t0.getTime() + 2 * 60 * 1000);
|
const t2 = new Date(t0.getTime() + 2 * 60 * 1000);
|
||||||
vi.setSystemTime(t2);
|
vi.setSystemTime(t2);
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||||
await runReminderCheck(t2);
|
await runReminderCheck(t2);
|
||||||
|
|
||||||
// Exactly one dispatch total: uid dedup prevents re-fire on ticks 2 and 3
|
// Exactly one dispatch total: uid:dtstartMs dedup prevents re-fire on ticks 2 and 3
|
||||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1);
|
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MISSED-TICK-RECOVERY: fires when scan runs 8 min before event after ideal tick was skipped', async () => {
|
it('RESCHEDULE (uid:dtstartMs): a rescheduled event (same uid, new dtstart) fires again', async () => {
|
||||||
|
// NOTIF-06: compound key uid:dtstartMs — new dtstartMs means new key → re-fires
|
||||||
const { db } = await import('../../src/db/client.js');
|
const { db } = await import('../../src/db/client.js');
|
||||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
// Event at 10:15:00Z. The ideal 15-min scan (10:00:00Z) was missed.
|
const now = new Date('2026-06-15T10:00:00Z');
|
||||||
// Call at 10:07:00Z — event is 8 min out, still > now and <= now+16min.
|
vi.setSystemTime(now);
|
||||||
|
|
||||||
|
const originalDtstart = new Date('2026-06-15T10:15:00Z');
|
||||||
|
|
||||||
|
const row = makeEventRow({
|
||||||
|
uid: 'reschedule-uid',
|
||||||
|
dtstartUtc: originalDtstart,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
// First tick — fires
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||||
|
await runReminderCheck(now);
|
||||||
|
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1);
|
||||||
|
|
||||||
|
// Advance time so original dtstart is pruned; event rescheduled to a new dtstart
|
||||||
|
const futureNow = new Date('2026-06-15T10:20:00Z'); // past original dtstart
|
||||||
|
vi.setSystemTime(futureNow);
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([])); // empty (original started)
|
||||||
|
await runReminderCheck(futureNow); // prune old entry
|
||||||
|
|
||||||
|
// Now event has new dtstart (same uid, different dtstartMs) — must fire again
|
||||||
|
const rescheduledNow = new Date('2026-06-15T11:00:00Z');
|
||||||
|
const rescheduledDtstart = new Date('2026-06-15T11:15:00Z');
|
||||||
|
vi.setSystemTime(rescheduledNow);
|
||||||
|
const rescheduledRow = makeEventRow({
|
||||||
|
uid: 'reschedule-uid',
|
||||||
|
dtstartUtc: rescheduledDtstart,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
|
});
|
||||||
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([rescheduledRow]));
|
||||||
|
await runReminderCheck(rescheduledNow);
|
||||||
|
|
||||||
|
// Must fire again — new compound key uid:rescheduledDtstartMs
|
||||||
|
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('MISSED-TICK-RECOVERY: fires when scan runs inside the 60s catch-up window after ideal tick was skipped', async () => {
|
||||||
|
const { db } = await import('../../src/db/client.js');
|
||||||
|
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||||
|
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||||
|
|
||||||
|
// Event at 10:15:00Z, 15-min lead → fire time 10:00:00Z.
|
||||||
|
// Ideal scan at 10:00Z was missed. Call at 10:00:45Z — still in (fire-60s, fire] window.
|
||||||
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
||||||
const recoveryNow = new Date('2026-06-15T10:07:00Z');
|
const recoveryNow = new Date('2026-06-15T10:00:45Z'); // 45s after ideal fire time
|
||||||
|
|
||||||
vi.setSystemTime(recoveryNow);
|
vi.setSystemTime(recoveryNow);
|
||||||
|
|
||||||
const sub = {
|
|
||||||
id: 1,
|
|
||||||
userId: 1,
|
|
||||||
endpoint: 'https://push.example.com/missed',
|
|
||||||
p256dh: 'k',
|
|
||||||
auth: 'a',
|
|
||||||
};
|
|
||||||
const row = makeEventRow({
|
const row = makeEventRow({
|
||||||
uid: 'missed-tick-uid',
|
uid: 'missed-tick-uid',
|
||||||
dtstartUtc: eventDtstart,
|
dtstartUtc: eventDtstart,
|
||||||
subId: sub.id,
|
reminderLeadMinutes: 15,
|
||||||
subUserId: sub.userId,
|
subId: 1,
|
||||||
subEndpoint: sub.endpoint,
|
subUserId: 1,
|
||||||
subP256dh: sub.p256dh,
|
subEndpoint: 'https://push.example.com/missed',
|
||||||
subAuth: sub.auth,
|
subP256dh: 'k',
|
||||||
|
subAuth: 'a',
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||||
@@ -276,6 +397,7 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
|||||||
makeEventRow({
|
makeEventRow({
|
||||||
uid: 'fanout-uid',
|
uid: 'fanout-uid',
|
||||||
dtstartUtc,
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: 1,
|
subId: 1,
|
||||||
subUserId: 1,
|
subUserId: 1,
|
||||||
subEndpoint: 'https://push.example.com/1',
|
subEndpoint: 'https://push.example.com/1',
|
||||||
@@ -285,6 +407,7 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
|||||||
makeEventRow({
|
makeEventRow({
|
||||||
uid: 'fanout-uid',
|
uid: 'fanout-uid',
|
||||||
dtstartUtc,
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: 2,
|
subId: 2,
|
||||||
subUserId: 2,
|
subUserId: 2,
|
||||||
subEndpoint: 'https://push.example.com/2',
|
subEndpoint: 'https://push.example.com/2',
|
||||||
@@ -336,6 +459,7 @@ describe('reminderScheduler — WR-01: mark-sent after dispatch', () => {
|
|||||||
uid: 'wr01-dedup-uid',
|
uid: 'wr01-dedup-uid',
|
||||||
title: 'WR-01 dedup event',
|
title: 'WR-01 dedup event',
|
||||||
dtstartUtc: new Date('2026-06-15T10:15:00Z'),
|
dtstartUtc: new Date('2026-06-15T10:15:00Z'),
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: sub.id,
|
subId: sub.id,
|
||||||
subUserId: sub.userId,
|
subUserId: sub.userId,
|
||||||
subEndpoint: sub.endpoint,
|
subEndpoint: sub.endpoint,
|
||||||
@@ -389,6 +513,7 @@ describe('reminderScheduler — CR-01: sentReminders Map pruning', () => {
|
|||||||
uid,
|
uid,
|
||||||
title: 'Prune test event',
|
title: 'Prune test event',
|
||||||
dtstartUtc: eventDtstart,
|
dtstartUtc: eventDtstart,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: sub.id,
|
subId: sub.id,
|
||||||
subUserId: sub.userId,
|
subUserId: sub.userId,
|
||||||
subEndpoint: sub.endpoint,
|
subEndpoint: sub.endpoint,
|
||||||
@@ -445,6 +570,7 @@ describe('reminderScheduler — T-05-19: per-subscription error isolation', () =
|
|||||||
makeEventRow({
|
makeEventRow({
|
||||||
uid: 'iso-uid',
|
uid: 'iso-uid',
|
||||||
dtstartUtc,
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: 1,
|
subId: 1,
|
||||||
subUserId: 1,
|
subUserId: 1,
|
||||||
subEndpoint: 'https://push.example.com/1',
|
subEndpoint: 'https://push.example.com/1',
|
||||||
@@ -454,6 +580,7 @@ describe('reminderScheduler — T-05-19: per-subscription error isolation', () =
|
|||||||
makeEventRow({
|
makeEventRow({
|
||||||
uid: 'iso-uid',
|
uid: 'iso-uid',
|
||||||
dtstartUtc,
|
dtstartUtc,
|
||||||
|
reminderLeadMinutes: 15,
|
||||||
subId: 2,
|
subId: 2,
|
||||||
subUserId: 2,
|
subUserId: 2,
|
||||||
subEndpoint: 'https://push.example.com/2',
|
subEndpoint: 'https://push.example.com/2',
|
||||||
|
|||||||
Reference in New Issue
Block a user