diff --git a/src/lib/export/backup-plan.ts b/src/lib/export/backup-plan.ts index be2bd5208..ef5dee16d 100644 --- a/src/lib/export/backup-plan.ts +++ b/src/lib/export/backup-plan.ts @@ -405,7 +405,9 @@ export const NOT_IN_BACKUP_MODELS: Readonly> = { // Ledgers whose meaning is local to the deployment. PushAttempt: - "A 90-day delivery ledger for this host's sends, and since v1.33.1 also the anchor rows the reminder dedup writes under a sentinel channel. Both describe what this instance did; restoring either onto another host would assert sends it never made, and the dedup anchors would suppress reminders that host still owes.", + "A 90-day delivery ledger for this host's sends. Restoring it onto another host would assert sends it never made.", + NotificationEvent: + "A local notification dedup anchor. Restoring it onto another host would suppress reminders and safety notices that host still owes.", AuditLog: "An append-only record of actions taken on this instance. Merging one instance's audit trail into another's would make the trail untrue.", CoachUsage: diff --git a/src/lib/jobs/__tests__/medication-reminder-dedup.test.ts b/src/lib/jobs/__tests__/medication-reminder-dedup.test.ts index e11b6912f..1a0607e77 100644 --- a/src/lib/jobs/__tests__/medication-reminder-dedup.test.ts +++ b/src/lib/jobs/__tests__/medication-reminder-dedup.test.ts @@ -9,7 +9,7 @@ * only when a Telegram send succeeds. A user on email / APNs / ntfy * therefore never had a ledger row, so the 15-minute tick re-fired the * same overdue notice for the rest of the day — roughly fifty mails per - * missed dose. The anchor now lives in `push_attempts` and is written + * missed dose. The anchor now lives in `notification_events` and is written * for every dispatched slot regardless of channel outcome. * * 2. **Coarser must not mean quieter.** The dedup is per phase and per @@ -24,16 +24,14 @@ */ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; -interface FakePushAttempt { - userId: string; - channel: string; +interface FakeNotificationEvent { + recordUserId: string; eventType: string; - result: string; - reason: string | null; + dedupKey: string; createdAt: Date; } -const pushAttempts: FakePushAttempt[] = []; +const notificationEvents: FakeNotificationEvent[] = []; const prismaMock = { telegramScheduledDeletion: { @@ -56,11 +54,11 @@ const prismaMock = { telegramReminderMessage: { findUnique: vi.fn().mockResolvedValue(null), }, - pushAttempt: { + notificationEvent: { create: vi.fn( - async ({ data }: { data: Omit }) => { - pushAttempts.push({ ...data, createdAt: new Date() }); - return { id: `pa-${pushAttempts.length}` }; + async ({ data }: { data: Omit }) => { + notificationEvents.push({ ...data, createdAt: new Date() }); + return { id: `ne-${notificationEvents.length}` }; }, ), findFirst: vi.fn( @@ -68,17 +66,17 @@ const prismaMock = { where, }: { where: { - userId: string; + recordUserId: string; eventType: string; - reason: string; + dedupKey: string; createdAt?: { gte: Date }; }; }) => - pushAttempts.find( + notificationEvents.find( (row) => - row.userId === where.userId && + row.recordUserId === where.recordUserId && row.eventType === where.eventType && - row.reason === where.reason && + row.dedupKey === where.dedupKey && (where.createdAt?.gte === undefined || row.createdAt >= where.createdAt.gte), ) ?? null, @@ -189,7 +187,7 @@ async function tickAt(iso: string): Promise { beforeEach(() => { vi.clearAllMocks(); - pushAttempts.length = 0; + notificationEvents.length = 0; vi.useFakeTimers(); prismaMock.telegramScheduledDeletion.findMany.mockResolvedValue([]); prismaMock.telegramPromptContext.deleteMany.mockResolvedValue({ count: 0 }); @@ -331,8 +329,8 @@ describe("issue #664 — dedup follows the scheduled occurrence across midnight" await tickAt("2026-07-28T22:30:00.000Z"); expect(dispatchNotification).toHaveBeenCalledTimes(1); - expect(pushAttempts).toHaveLength(1); - expect(pushAttempts[0]?.reason).toBe( + expect(notificationEvents).toHaveLength(1); + expect(notificationEvents[0]?.dedupKey).toBe( "med:med-1:sched-1:23:45:YELLOW:2026-07-28", ); });