From 69f97b268ca256c3b259440a456710c0c29b9ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 6 Aug 2026 04:48:47 +0200 Subject: [PATCH] fix: erase notification anchors with record data - remove record event anchors during data deletion\n- retain sustained safety dedup coverage --- src/lib/data-wipe/wipe-plan.ts | 2 ++ .../__tests__/safety-floor-sustained.test.ts | 32 ++++++++++++++----- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/lib/data-wipe/wipe-plan.ts b/src/lib/data-wipe/wipe-plan.ts index b30dbb111..ae0ddc698 100644 --- a/src/lib/data-wipe/wipe-plan.ts +++ b/src/lib/data-wipe/wipe-plan.ts @@ -161,6 +161,7 @@ export const WIPE_MODELS = [ "NotificationChannel", "PushSubscription", "PushAttempt", + "NotificationEvent", "Device", "TelegramScheduledDeletion", "TelegramPromptContext", @@ -421,6 +422,7 @@ export const WIPE_OWNER_FIELDS: Readonly> = { // attribution and must not let deleting a manager erase another record's // import history. MedicationIntakeImportJob: ["recordUserId"], + NotificationEvent: ["recordUserId"], // Both sides. Wiping only the grantor side would leave the account still // holding read access to other people's records after it asked for // everything of its own to be deleted. diff --git a/src/lib/illness/__tests__/safety-floor-sustained.test.ts b/src/lib/illness/__tests__/safety-floor-sustained.test.ts index bc874e075..9c9866cc2 100644 --- a/src/lib/illness/__tests__/safety-floor-sustained.test.ts +++ b/src/lib/illness/__tests__/safety-floor-sustained.test.ts @@ -6,14 +6,14 @@ * opened the episode's correlation tab. These tests pin the on-write seam: * a sustained run escalates on INGEST through the SAME illness red-flag push, * with the SAME run length (RED_FLAG_RUN_DAYS) and absolute floors the detector - * uses, and the 24h ledger dedupe stops a 4th reading re-firing the alarm. + * uses, and the 24h record-event dedupe stops a 4th reading re-firing the alarm. * * Calendar-day bucketing runs against TZ=UTC (the gate's contract). */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -/** In-memory pushAttempt ledger so the 24h dedupe behaves across calls. */ -const ledger: Array<{ userId: string; reason: string }> = []; +/** In-memory record event stream so the 24h dedupe behaves across calls. */ +const ledger: Array<{ recordUserId: string; dedupKey: string }> = []; const measurementFindMany = vi.fn(); const dispatchMock = vi.fn(); @@ -22,15 +22,31 @@ vi.mock("@/lib/db", () => ({ measurement: { findMany: (...a: unknown[]) => measurementFindMany(...a), }, - pushAttempt: { - findFirst: ({ where }: { where: { userId: string; reason: string } }) => { + user: { + findUnique: vi.fn().mockResolvedValue({ managedProfileAt: null }), + }, + notificationEvent: { + findFirst: ({ + where, + }: { + where: { recordUserId: string; dedupKey: string }; + }) => { const hit = ledger.find( - (r) => r.userId === where.userId && r.reason === where.reason, + (r) => + r.recordUserId === where.recordUserId && + r.dedupKey === where.dedupKey, ); return Promise.resolve(hit ? { id: "prior" } : null); }, - create: ({ data }: { data: { userId: string; reason: string } }) => { - ledger.push({ userId: data.userId, reason: data.reason }); + create: ({ + data, + }: { + data: { recordUserId: string; dedupKey: string }; + }) => { + ledger.push({ + recordUserId: data.recordUserId, + dedupKey: data.dedupKey, + }); return Promise.resolve({}); }, },