feat(11-02): Task 2 — humanizeLeadMinutes tests + body dispatch assertion (D-09)

- Add 8 bucket tests: 30→'30 min', 59→'59 min', 60→'1 hr', 90→'1 hr',
  120→'2 hrs', 1440→'1 day', 2880→'2 days', 10080→'7 days'
- Add body-in-dispatch test: 1440-min lead → body='Starts in 1 day'
  (driven by configured lead, not live minutes-to-start delta)
- humanizeLeadMinutes implementation already committed in Task 1 GREEN
- All 23 tests GREEN
This commit is contained in:
Lucas Berger
2026-06-13 22:22:03 -04:00
parent 62d3f58684
commit 57f9d67685
@@ -567,6 +567,91 @@ describe('reminderScheduler — CR-01: sentReminders Map pruning', () => {
});
});
// ── D-09: humanizeLeadMinutes body formatter ─────────────────────────────────
describe('humanizeLeadMinutes — D-09 humanized push body formatter', () => {
it('maps 30 min → "Starts in 30 min"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(30)).toBe('Starts in 30 min');
});
it('maps 59 min → "Starts in 59 min" (< 60 bucket)', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(59)).toBe('Starts in 59 min');
});
it('maps 60 min → "Starts in 1 hour"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(60)).toBe('Starts in 1 hour');
});
it('maps 90 min → "Starts in 1 hour" (60119 bucket, not 2 hours)', async () => {
// Math.round(90/60)=2 would give "2 hours" — branch ordering must prevent this
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(90)).toBe('Starts in 1 hour');
});
it('maps 120 min → "Starts in 2 hours"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(120)).toBe('Starts in 2 hours');
});
it('maps 1440 min → "Starts in 1 day"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(1440)).toBe('Starts in 1 day');
});
it('maps 2880 min → "Starts in 2 days"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(2880)).toBe('Starts in 2 days');
});
it('maps 10080 min → "Starts in 7 days"', async () => {
const { humanizeLeadMinutes } = await import('../../src/broker/reminderScheduler.js');
expect(humanizeLeadMinutes(10080)).toBe('Starts in 7 days');
});
});
describe('reminderScheduler — D-09: humanized push body in dispatch', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.resetModules();
});
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
});
it('dispatched notification body is humanized from configured lead (not live minutes-to-start)', async () => {
// D-09: body driven by event.reminderLeadMinutes (DB ground truth), not by (dtstart - now)
// A 1440-min (1 day) lead event should produce "Starts in 1 day", not "Starts in 30 min"
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');
// Event exactly 1440 min (1 day) from now → fire time = dtstartUtc - 1440min = now
const dtstartUtc = new Date('2026-06-16T10:00:00Z'); // tomorrow 10:00Z
const row = makeEventRow({
uid: 'humanize-body-uid',
title: 'Meeting Tomorrow',
dtstartUtc,
reminderLeadMinutes: 1440,
});
mockTwoQueries(vi.mocked(db), [row]);
await runReminderCheck(now);
expect(vi.mocked(dispatchPush)).toHaveBeenCalledOnce();
const [, payload] = vi.mocked(dispatchPush).mock.calls[0]!;
// Body must be "Starts in 1 day" (configured lead), not "Starts in 1470 min" (live delta)
expect((payload as { body: string }).body).toBe('Starts in 1 day');
});
});
// ── T-05-19: per-subscription error isolation ────────────────────────────────
describe('reminderScheduler — T-05-19: per-subscription error isolation', () => {