From 8702d85c9db6a5e4c475d2cc09a8f48cacc16d66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 10 Sep 2026 01:05:03 +0200 Subject: [PATCH] test(notifications): pin aps.mutable-content on the medication reminder The client's notification service extension replaces the medication name on the lock screen, and iOS routes a payload through an extension only when the alert carries `mutable-content`. The dispatcher has set the flag on every alert it builds since the category work for the v0.5.4 client, but the only assertion on it sat inside a category test, where dropping it would have failed nothing and the name would have quietly reappeared on the lock screen of a device that asked for it to be hidden. Two cases now say it out loud: the medication reminder carries the flag, and so does another event type, because that is the shipped behaviour and narrowing it would take a capability away from the client rather than tidy something up. --- .../senders/__tests__/apns.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/lib/notifications/senders/__tests__/apns.test.ts b/src/lib/notifications/senders/__tests__/apns.test.ts index 7d567c230..c7c68b849 100644 --- a/src/lib/notifications/senders/__tests__/apns.test.ts +++ b/src/lib/notifications/senders/__tests__/apns.test.ts @@ -630,6 +630,49 @@ describe("sendViaApns — dispatcher fan-out", () => { expect(note.payload.eventType).toBe("MEDICATION_REMINDER"); }); + // The iOS Notification Service Extension that replaces the medication name + // on the lock screen runs only when the payload carries + // `aps.mutable-content = 1`. The category test above happens to touch the + // flag; this pins it on its own, because losing it would not fail a + // category assertion and the name would silently reappear on the lock + // screen of a device that asked for it to be hidden. + it("marks a MEDICATION_REMINDER mutable so the client extension can rewrite the alert", async () => { + vi.mocked(prisma.device.findMany).mockResolvedValueOnce([ + { id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" }, + ] as never); + sendMock.mockResolvedValueOnce({ sent: [{ device: "tok-a" }], failed: [] }); + + await sendViaApns("u-1", { + title: "t", + message: "m", + eventType: "MEDICATION_REMINDER", + metadata: { medicationId: "med-1" }, + }); + + expect(sendMock.mock.calls[0][0].mutableContent).toBe(true); + }); + + // Not a medication-only flag: `sendViaApns` sets it on every alert it + // builds, so an extension may enrich whichever event types the client + // registers for. Recorded as an assertion rather than left to be assumed + // from the source, because narrowing it to one branch would take a + // capability away from the client and should read as the contract change it + // is, not as a tidy-up. + it("marks every alert event type mutable, not only the medication branch", async () => { + vi.mocked(prisma.device.findMany).mockResolvedValueOnce([ + { id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" }, + ] as never); + sendMock.mockResolvedValueOnce({ sent: [{ device: "tok-a" }], failed: [] }); + + await sendViaApns("u-1", { + title: "t", + message: "m", + eventType: "MEASUREMENT_REMINDER", + }); + + expect(sendMock.mock.calls[0][0].mutableContent).toBe(true); + }); + it("omits action metadata and categories for a managed Guardian", async () => { vi.mocked(prisma.device.findMany).mockResolvedValueOnce([ { id: "d1", apnsToken: "tok-a", apnsEnvironment: "sandbox" },