From 57f9d676854ae4068c4460f1e73a8119fd0e4270 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Sat, 13 Jun 2026 22:22:03 -0400 Subject: [PATCH] =?UTF-8?q?feat(11-02):=20Task=202=20=E2=80=94=20humanizeL?= =?UTF-8?q?eadMinutes=20tests=20+=20body=20dispatch=20assertion=20(D-09)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../tests/broker/reminderScheduler.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/apps/api/tests/broker/reminderScheduler.test.ts b/apps/api/tests/broker/reminderScheduler.test.ts index 3ca3c57..be04096 100644 --- a/apps/api/tests/broker/reminderScheduler.test.ts +++ b/apps/api/tests/broker/reminderScheduler.test.ts @@ -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" (60–119 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', () => {