style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
@@ -19,19 +19,19 @@
|
||||
* Run: pnpm --filter @familysync/api exec vitest run tests/broker/reminderScheduler.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Mock the DB so we can control what events are returned
|
||||
vi.mock('../../src/db/client.js', () => ({
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
// Mock pushDispatcher so no real push occurs
|
||||
vi.mock('../../src/lib/pushDispatcher.js', () => ({
|
||||
dispatchPush: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
}));
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -44,18 +44,18 @@ function makeSelectMock(rows: unknown[]) {
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
} as never
|
||||
} as never;
|
||||
}
|
||||
|
||||
function makeEventRow(overrides: {
|
||||
uid?: string
|
||||
title?: string
|
||||
dtstartUtc: Date
|
||||
subId?: number | null
|
||||
subUserId?: number | null
|
||||
subEndpoint?: string
|
||||
subP256dh?: string
|
||||
subAuth?: string
|
||||
uid?: string;
|
||||
title?: string;
|
||||
dtstartUtc: Date;
|
||||
subId?: number | null;
|
||||
subUserId?: number | null;
|
||||
subEndpoint?: string;
|
||||
subP256dh?: string;
|
||||
subAuth?: string;
|
||||
}) {
|
||||
return {
|
||||
uid: overrides.uid ?? 'test-uid-1',
|
||||
@@ -68,162 +68,183 @@ function makeEventRow(overrides: {
|
||||
subEndpoint: overrides.subEndpoint ?? 'https://push.example.com/1',
|
||||
subP256dh: overrides.subP256dh ?? 'p256dh-key',
|
||||
subAuth: overrides.subAuth ?? 'auth-secret',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ── Filtering tests (D-05 / D-07) ────────────────────────────────────────────
|
||||
|
||||
describe('reminderScheduler — shared+timed event filtering (D-05/D-07)', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not dispatch reminders for all-day events (D-07)', async () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { db } = await import('../../src/db/client.js');
|
||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||
|
||||
// allDay=true events are excluded by the SQL WHERE; simulate by returning empty rows
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]))
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||
|
||||
await runReminderCheck()
|
||||
await runReminderCheck();
|
||||
|
||||
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 () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { db } = await import('../../src/db/client.js');
|
||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||
|
||||
// isShared=false events are excluded by the SQL WHERE; simulate by returning empty rows
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]))
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||
|
||||
await runReminderCheck()
|
||||
await runReminderCheck();
|
||||
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not dispatch for an event whose start has already passed (dtstart <= now)', async () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { db } = await import('../../src/db/client.js');
|
||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||
|
||||
// gt(dtstartUtc, now) excludes already-started events; simulate by returning empty rows
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]))
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||
|
||||
await runReminderCheck(now)
|
||||
await runReminderCheck(now);
|
||||
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── D-16: empty subscriptions — zero sends / no crash ────────────────────────
|
||||
|
||||
describe('reminderScheduler — D-16: empty push_subscriptions', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('produces zero sends and does not crash when push_subscriptions is empty (D-16)', async () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { db } = await import('../../src/db/client.js');
|
||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||
|
||||
// Cross-join with empty push_subscriptions returns no rows
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]))
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||
|
||||
await expect(runReminderCheck(now)).resolves.toBeUndefined()
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
await expect(runReminderCheck(now)).resolves.toBeUndefined();
|
||||
expect(vi.mocked(dispatchPush)).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ── Catch-up / missed-tick recovery ──────────────────────────────────────────
|
||||
|
||||
describe('reminderScheduler — catch-up window and missed-tick recovery', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('SINGLE-FIRE: dispatches exactly once across three consecutive ticks while event is in window', 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')
|
||||
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 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)
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z')
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
||||
|
||||
const sub = { id: 1, userId: 1, endpoint: 'https://push.example.com/1', p256dh: 'k', auth: 'a' }
|
||||
const sub = {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
endpoint: 'https://push.example.com/1',
|
||||
p256dh: 'k',
|
||||
auth: 'a',
|
||||
};
|
||||
|
||||
function rowForNow() {
|
||||
return makeEventRow({ uid: 'single-fire-uid', dtstartUtc: eventDtstart, ...sub, subId: sub.id, subUserId: sub.userId, subEndpoint: sub.endpoint, subP256dh: sub.p256dh, subAuth: sub.auth })
|
||||
return makeEventRow({
|
||||
uid: 'single-fire-uid',
|
||||
dtstartUtc: eventDtstart,
|
||||
...sub,
|
||||
subId: sub.id,
|
||||
subUserId: sub.userId,
|
||||
subEndpoint: sub.endpoint,
|
||||
subP256dh: sub.p256dh,
|
||||
subAuth: sub.auth,
|
||||
});
|
||||
}
|
||||
|
||||
// Tick at t0 (event 15 min out)
|
||||
vi.setSystemTime(t0)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]))
|
||||
await runReminderCheck(t0)
|
||||
vi.setSystemTime(t0);
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||
await runReminderCheck(t0);
|
||||
|
||||
// Tick at t0+1min (event 14 min out — still in window, same uid)
|
||||
const t1 = new Date(t0.getTime() + 60 * 1000)
|
||||
vi.setSystemTime(t1)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]))
|
||||
await runReminderCheck(t1)
|
||||
const t1 = new Date(t0.getTime() + 60 * 1000);
|
||||
vi.setSystemTime(t1);
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||
await runReminderCheck(t1);
|
||||
|
||||
// Tick at t0+2min (event 13 min out — still in window, same uid)
|
||||
const t2 = new Date(t0.getTime() + 2 * 60 * 1000)
|
||||
vi.setSystemTime(t2)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]))
|
||||
await runReminderCheck(t2)
|
||||
const t2 = new Date(t0.getTime() + 2 * 60 * 1000);
|
||||
vi.setSystemTime(t2);
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([rowForNow()]));
|
||||
await runReminderCheck(t2);
|
||||
|
||||
// Exactly one dispatch total: uid 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 () => {
|
||||
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 { 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. The ideal 15-min scan (10:00:00Z) was missed.
|
||||
// Call at 10:07:00Z — event is 8 min out, still > now and <= now+16min.
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z')
|
||||
const recoveryNow = new Date('2026-06-15T10:07:00Z')
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
||||
const recoveryNow = new Date('2026-06-15T10:07:00Z');
|
||||
|
||||
vi.setSystemTime(recoveryNow)
|
||||
vi.setSystemTime(recoveryNow);
|
||||
|
||||
const sub = { id: 1, userId: 1, endpoint: 'https://push.example.com/missed', p256dh: 'k', auth: 'a' }
|
||||
const sub = {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
endpoint: 'https://push.example.com/missed',
|
||||
p256dh: 'k',
|
||||
auth: 'a',
|
||||
};
|
||||
const row = makeEventRow({
|
||||
uid: 'missed-tick-uid',
|
||||
dtstartUtc: eventDtstart,
|
||||
@@ -232,63 +253,85 @@ describe('reminderScheduler — catch-up window and missed-tick recovery', () =>
|
||||
subEndpoint: sub.endpoint,
|
||||
subP256dh: sub.p256dh,
|
||||
subAuth: sub.auth,
|
||||
})
|
||||
});
|
||||
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]))
|
||||
await runReminderCheck(recoveryNow)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([row]));
|
||||
await runReminderCheck(recoveryNow);
|
||||
|
||||
// Reminder must fire even though the ideal-mark tick was skipped
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('dispatches to all subscribers for a single event (fan-out)', async () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { 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:15:00Z')
|
||||
const dtstartUtc = new Date('2026-06-15T10:15:00Z');
|
||||
|
||||
const rows = [
|
||||
makeEventRow({ uid: 'fanout-uid', dtstartUtc, subId: 1, subUserId: 1, subEndpoint: 'https://push.example.com/1', subP256dh: 'k1', subAuth: 'a1' }),
|
||||
makeEventRow({ uid: 'fanout-uid', dtstartUtc, subId: 2, subUserId: 2, subEndpoint: 'https://push.example.com/2', subP256dh: 'k2', subAuth: 'a2' }),
|
||||
]
|
||||
makeEventRow({
|
||||
uid: 'fanout-uid',
|
||||
dtstartUtc,
|
||||
subId: 1,
|
||||
subUserId: 1,
|
||||
subEndpoint: 'https://push.example.com/1',
|
||||
subP256dh: 'k1',
|
||||
subAuth: 'a1',
|
||||
}),
|
||||
makeEventRow({
|
||||
uid: 'fanout-uid',
|
||||
dtstartUtc,
|
||||
subId: 2,
|
||||
subUserId: 2,
|
||||
subEndpoint: 'https://push.example.com/2',
|
||||
subP256dh: 'k2',
|
||||
subAuth: 'a2',
|
||||
}),
|
||||
];
|
||||
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock(rows))
|
||||
await runReminderCheck(now)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock(rows));
|
||||
await runReminderCheck(now);
|
||||
|
||||
// One dispatch per subscriber
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(2)
|
||||
})
|
||||
})
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── WR-01: mark-sent after dispatch ──────────────────────────────────────────
|
||||
|
||||
describe('reminderScheduler — WR-01: mark-sent after dispatch', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not re-dispatch the same uid after a successful dispatch in the same module instance', async () => {
|
||||
// WR-01: sentReminders.set(uid) is called AFTER the fan-out loop completes.
|
||||
// After a successful dispatch the uid is recorded; a second run with the same
|
||||
// module instance must not re-fire for the same uid.
|
||||
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 { db } = await import('../../src/db/client.js');
|
||||
const { dispatchPush } = await import('../../src/lib/pushDispatcher.js');
|
||||
const { runReminderCheck } = await import('../../src/broker/reminderScheduler.js');
|
||||
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
const now = new Date('2026-06-15T10:00:00Z');
|
||||
vi.setSystemTime(now);
|
||||
|
||||
const sub = { id: 1, userId: 1, endpoint: 'https://push.example.com/1', p256dh: 'k', auth: 'a' }
|
||||
const sub = {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
endpoint: 'https://push.example.com/1',
|
||||
p256dh: 'k',
|
||||
auth: 'a',
|
||||
};
|
||||
const eventRow = makeEventRow({
|
||||
uid: 'wr01-dedup-uid',
|
||||
title: 'WR-01 dedup event',
|
||||
@@ -298,43 +341,49 @@ describe('reminderScheduler — WR-01: mark-sent after dispatch', () => {
|
||||
subEndpoint: sub.endpoint,
|
||||
subP256dh: sub.p256dh,
|
||||
subAuth: sub.auth,
|
||||
})
|
||||
});
|
||||
|
||||
vi.mocked(dispatchPush).mockResolvedValue(undefined)
|
||||
vi.mocked(dispatchPush).mockResolvedValue(undefined);
|
||||
|
||||
// First run — dispatch succeeds; uid is recorded after the loop
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]))
|
||||
await runReminderCheck(now)
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1) // dispatched once
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]));
|
||||
await runReminderCheck(now);
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1); // dispatched once
|
||||
|
||||
// Second run with same now — uid is in sentReminders; should NOT re-dispatch
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]))
|
||||
await runReminderCheck(now)
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1) // still 1 — deduped
|
||||
})
|
||||
})
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]));
|
||||
await runReminderCheck(now);
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1); // still 1 — deduped
|
||||
});
|
||||
});
|
||||
|
||||
// ── CR-01: started-event pruning ─────────────────────────────────────────────
|
||||
|
||||
describe('reminderScheduler — CR-01: sentReminders Map pruning', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('prunes started-event entries so a different future event with the same uid can fire again', 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')
|
||||
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 sub = { id: 1, userId: 1, endpoint: 'https://push.example.com/1', p256dh: 'k', auth: 'a' }
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z')
|
||||
const uid = 'prune-test-uid'
|
||||
const sub = {
|
||||
id: 1,
|
||||
userId: 1,
|
||||
endpoint: 'https://push.example.com/1',
|
||||
p256dh: 'k',
|
||||
auth: 'a',
|
||||
};
|
||||
const eventDtstart = new Date('2026-06-15T10:15:00Z');
|
||||
const uid = 'prune-test-uid';
|
||||
|
||||
const eventRow = makeEventRow({
|
||||
uid,
|
||||
@@ -345,65 +394,81 @@ describe('reminderScheduler — CR-01: sentReminders Map pruning', () => {
|
||||
subEndpoint: sub.endpoint,
|
||||
subP256dh: sub.p256dh,
|
||||
subAuth: sub.auth,
|
||||
})
|
||||
});
|
||||
|
||||
// Tick at t0 (now=10:00): event 15 min out — fires
|
||||
const t0 = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(t0)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]))
|
||||
await runReminderCheck(t0)
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1) // fired
|
||||
const t0 = new Date('2026-06-15T10:00:00Z');
|
||||
vi.setSystemTime(t0);
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]));
|
||||
await runReminderCheck(t0);
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1); // fired
|
||||
|
||||
// Same t0 — uid still in Map — must NOT re-fire
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]))
|
||||
await runReminderCheck(t0)
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1) // still 1
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([eventRow]));
|
||||
await runReminderCheck(t0);
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1); // still 1
|
||||
|
||||
// Advance past dtstart (now=10:20): event has started; CR-01 prunes the uid entry.
|
||||
// The SQL WHERE gt(dtstartUtc, now) would return no rows, so simulate empty.
|
||||
const tPast = new Date('2026-06-15T10:20:00Z')
|
||||
vi.setSystemTime(tPast)
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]))
|
||||
await runReminderCheck(tPast)
|
||||
const tPast = new Date('2026-06-15T10:20:00Z');
|
||||
vi.setSystemTime(tPast);
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock([]));
|
||||
await runReminderCheck(tPast);
|
||||
// Pruning fires; dispatch count stays at 1
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1)
|
||||
})
|
||||
})
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ── T-05-19: per-subscription error isolation ────────────────────────────────
|
||||
|
||||
describe('reminderScheduler — T-05-19: per-subscription error isolation', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.resetModules()
|
||||
})
|
||||
vi.useFakeTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('continues dispatching to remaining subscribers when one subscription throws', async () => {
|
||||
const now = new Date('2026-06-15T10:00:00Z')
|
||||
vi.setSystemTime(now)
|
||||
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 { 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:15:00Z')
|
||||
const dtstartUtc = new Date('2026-06-15T10:15:00Z');
|
||||
const rows = [
|
||||
makeEventRow({ uid: 'iso-uid', dtstartUtc, subId: 1, subUserId: 1, subEndpoint: 'https://push.example.com/1', subP256dh: 'k1', subAuth: 'a1' }),
|
||||
makeEventRow({ uid: 'iso-uid', dtstartUtc, subId: 2, subUserId: 2, subEndpoint: 'https://push.example.com/2', subP256dh: 'k2', subAuth: 'a2' }),
|
||||
]
|
||||
makeEventRow({
|
||||
uid: 'iso-uid',
|
||||
dtstartUtc,
|
||||
subId: 1,
|
||||
subUserId: 1,
|
||||
subEndpoint: 'https://push.example.com/1',
|
||||
subP256dh: 'k1',
|
||||
subAuth: 'a1',
|
||||
}),
|
||||
makeEventRow({
|
||||
uid: 'iso-uid',
|
||||
dtstartUtc,
|
||||
subId: 2,
|
||||
subUserId: 2,
|
||||
subEndpoint: 'https://push.example.com/2',
|
||||
subP256dh: 'k2',
|
||||
subAuth: 'a2',
|
||||
}),
|
||||
];
|
||||
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock(rows))
|
||||
vi.mocked(db.select).mockReturnValue(makeSelectMock(rows));
|
||||
// First subscriber throws; second should still be attempted
|
||||
vi.mocked(dispatchPush)
|
||||
.mockRejectedValueOnce(new Error('network error'))
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockResolvedValueOnce(undefined);
|
||||
|
||||
await expect(runReminderCheck(now)).resolves.toBeUndefined()
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(2) // both attempted
|
||||
})
|
||||
})
|
||||
await expect(runReminderCheck(now)).resolves.toBeUndefined();
|
||||
expect(vi.mocked(dispatchPush).mock.calls.length).toBe(2); // both attempted
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user