fix: suppress premature managed safety delivery

- keep direct safety notifications to ordinary self-delivery\n- move safety dedup anchors out of delivery diagnostics
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 04:41:40 +02:00
1 parent ce94f4773f
commit 027f2b4355
4 files changed
+98 -36

No files matched your search

@@ -10,10 +10,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const findFirstMock = vi.fn();
const createMock = vi.fn();
const dispatchMock = vi.fn();
const userFindUniqueMock = vi.fn();
vi.mock("@/lib/db", () => ({
prisma: {
pushAttempt: {
user: {
findUnique: (...a: unknown[]) => userFindUniqueMock(...a),
},
notificationEvent: {
findFirst: (...a: unknown[]) => findFirstMock(...a),
create: (...a: unknown[]) => createMock(...a),
},
@@ -45,6 +49,7 @@ const feverFlag: IllnessRedFlag = {
};
beforeEach(() => {
userFindUniqueMock.mockReset().mockResolvedValue({ managedProfileAt: null });
findFirstMock.mockReset().mockResolvedValue(null);
createMock.mockReset().mockResolvedValue({});
dispatchMock.mockReset().mockResolvedValue(undefined);
@@ -96,14 +101,34 @@ describe("notifyIllnessRedFlag", () => {
expect(createMock).not.toHaveBeenCalled();
});
it("stamps the ledger anchor before dispatching", async () => {
it("stamps the record event anchor before dispatching", async () => {
await notifyIllnessRedFlag({
userId: "u1",
episodeId: "e9",
redFlags: [feverFlag],
});
expect(createMock).toHaveBeenCalledTimes(1);
expect(createMock.mock.calls[0][0].data.reason).toContain("e9");
expect(createMock.mock.calls[0][0].data).toMatchObject({
recordUserId: "u1",
eventType: "SYSTEM_ALERT",
dedupKey: expect.stringContaining("e9"),
});
});
it("does not dispatch or write an anchor for a managed profile", async () => {
userFindUniqueMock.mockResolvedValueOnce({
managedProfileAt: new Date("2026-08-06T00:00:00.000Z"),
});
await notifyIllnessRedFlag({
userId: "managed-record",
episodeId: "e1",
redFlags: [feverFlag],
});
expect(findFirstMock).not.toHaveBeenCalled();
expect(createMock).not.toHaveBeenCalled();
expect(dispatchMock).not.toHaveBeenCalled();
});
it("never throws when dispatch fails", async () => {
@@ -11,10 +11,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const findFirstMock = vi.fn();
const createMock = vi.fn();
const dispatchMock = vi.fn();
const userFindUniqueMock = vi.fn();
vi.mock("@/lib/db", () => ({
prisma: {
pushAttempt: {
user: {
findUnique: (...a: unknown[]) => userFindUniqueMock(...a),
},
notificationEvent: {
findFirst: (...a: unknown[]) => findFirstMock(...a),
create: (...a: unknown[]) => createMock(...a),
},
@@ -50,6 +54,7 @@ const glucoseLowSymptomatic: SafetyFloorDecision = {
};
beforeEach(() => {
userFindUniqueMock.mockReset().mockResolvedValue({ managedProfileAt: null });
findFirstMock.mockReset().mockResolvedValue(null);
createMock.mockReset().mockResolvedValue({});
dispatchMock.mockReset().mockResolvedValue(undefined);
@@ -101,13 +106,26 @@ describe("notifySafetyFloor", () => {
expect(createMock).not.toHaveBeenCalled();
});
it("stamps the ledger anchor (keyed by reason) before dispatching", async () => {
it("stamps the record event anchor (keyed by reason) before dispatching", async () => {
await notifySafetyFloor({ userId: "u9", decision: bpHigh });
expect(createMock).toHaveBeenCalledTimes(1);
expect(createMock.mock.calls[0][0].data.reason).toContain(
"bp_hypertensive",
);
expect(createMock.mock.calls[0][0].data.eventType).toBe("SYSTEM_ALERT");
expect(createMock.mock.calls[0][0].data).toMatchObject({
recordUserId: "u9",
eventType: "SYSTEM_ALERT",
dedupKey: expect.stringContaining("bp_hypertensive"),
});
});
it("does not dispatch or write an anchor for a managed profile", async () => {
userFindUniqueMock.mockResolvedValueOnce({
managedProfileAt: new Date("2026-08-06T00:00:00.000Z"),
});
await notifySafetyFloor({ userId: "managed-record", decision: bpHigh });
expect(findFirstMock).not.toHaveBeenCalled();
expect(createMock).not.toHaveBeenCalled();
expect(dispatchMock).not.toHaveBeenCalled();
});
it("never throws when dispatch fails", async () => {
+24 -15
View File
@@ -15,8 +15,8 @@
* the strongest tier those channels support.
*
* Owner-scoped (the authenticated episode owner) and module-gated by the
* caller (`requireIllnessEnabled`). De-duplicated through the `pushAttempt`
* ledger: at most one escalation per episode per rolling window, so a
* caller (`requireIllnessEnabled`). De-duplicated through a record event:
* at most one escalation per episode per rolling window, so a
* correlation surface that is re-read repeatedly never re-fires the alarm.
*/
import { prisma } from "@/lib/db";
@@ -27,7 +27,7 @@ import type { IllnessRedFlag } from "@/lib/illness/correlation";
/** One escalation per episode per this window. */
const DEDUPE_WINDOW_MS = 24 * 60 * 60 * 1000;
/** Stable ledger reason prefix so the dedupe lookup is exact-matchable. */
/** Stable event key prefix so the dedupe lookup is exact-matchable. */
const LEDGER_REASON_PREFIX = "illness_red_flag:";
/**
@@ -44,13 +44,25 @@ export async function notifyIllnessRedFlag(input: {
if (redFlags.length === 0) return;
try {
// Plan 11 adds approved guardian fanout for this safety signal. Until
// then, managed profiles must not produce a direct delivery or anchor.
const user = await prisma.user.findUnique({
where: { id: userId },
select: { managedProfileAt: true },
});
if (user?.managedProfileAt) return;
// Dedupe: skip when an escalation for THIS episode already landed in the
// ledger within the window. The episode id rides the `reason` column so
// the lookup is exact without a schema change.
// record event stream within the window.
const reason = `${LEDGER_REASON_PREFIX}${episodeId}`;
const since = new Date(Date.now() - DEDUPE_WINDOW_MS);
const prior = await prisma.pushAttempt.findFirst({
where: { userId, reason, createdAt: { gte: since } },
const prior = await prisma.notificationEvent.findFirst({
where: {
recordUserId: userId,
eventType: "SYSTEM_ALERT",
dedupKey: reason,
createdAt: { gte: since },
},
select: { id: true },
});
if (prior) return;
@@ -62,16 +74,13 @@ export async function notifyIllnessRedFlag(input: {
? "illness.correlation.redFlagFever"
: "illness.correlation.redFlagSpo2";
// Stamp the ledger BEFORE dispatching so a slow/duplicate concurrent read
// can't double-fire. The senders write their own per-channel rows too;
// this synthetic row is purely the dedupe anchor.
await prisma.pushAttempt.create({
// Stamp the record event before dispatching so a slow/duplicate concurrent
// read cannot double-fire. Delivery attempts remain channel diagnostics.
await prisma.notificationEvent.create({
data: {
userId,
channel: "WEB_PUSH",
recordUserId: userId,
eventType: "SYSTEM_ALERT",
result: "skipped",
reason,
dedupKey: reason,
},
});
+22 -12
View File
@@ -13,7 +13,7 @@
* - The body always echoes the (already re-confirmed) reading value.
*
* Dedup: at most one push per (user, reason) per rolling 24h window, anchored
* on a synthetic `pushAttempt` row keyed by reason — so a user re-checking a
* on a record event keyed by reason — so a user re-checking a
* stubbornly-high reading repeatedly doesn't get spammed. Owner-scoped, never
* throws (a notification failure must not break the measurement write).
*/
@@ -31,7 +31,7 @@ import {
/** One escalation per (user, reason) per this window. */
const DEDUPE_WINDOW_MS = 24 * 60 * 60 * 1000;
/** Stable ledger reason prefix so the dedupe lookup is exact-matchable. */
/** Stable event key prefix so the dedupe lookup is exact-matchable. */
const LEDGER_REASON_PREFIX = "safety_floor:";
/**
@@ -113,24 +113,34 @@ export async function notifySafetyFloor(input: {
if (!decision) return;
try {
// Plan 11 adds approved guardian fanout for this safety signal. Until
// then, managed profiles must not produce a direct delivery or anchor.
const user = await prisma.user.findUnique({
where: { id: userId },
select: { managedProfileAt: true },
});
if (user?.managedProfileAt) return;
const reason = `${LEDGER_REASON_PREFIX}${decision.reason}`;
const since = new Date(Date.now() - DEDUPE_WINDOW_MS);
const prior = await prisma.pushAttempt.findFirst({
where: { userId, reason, createdAt: { gte: since } },
const prior = await prisma.notificationEvent.findFirst({
where: {
recordUserId: userId,
eventType: "SYSTEM_ALERT",
dedupKey: reason,
createdAt: { gte: since },
},
select: { id: true },
});
if (prior) return;
// Stamp the ledger BEFORE dispatching so a concurrent confirm can't
// double-fire. The senders write their own per-channel rows; this
// synthetic row is purely the dedupe anchor.
await prisma.pushAttempt.create({
// Stamp the record event before dispatching so a concurrent confirm
// cannot double-fire. Delivery attempts remain channel diagnostics.
await prisma.notificationEvent.create({
data: {
userId,
channel: "WEB_PUSH",
recordUserId: userId,
eventType: "SYSTEM_ALERT",
result: "skipped",
reason,
dedupKey: reason,
},
});