mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
fix: retain notification anchor boundaries
- classify local notification anchors outside backups\n- keep reminder dedup fixtures on record events
This commit is contained in:
1 parent
10a58a7e6a
commit
9c4c7b2b7d
2 files changed
+20
-20
No files matched your search
@@ -405,7 +405,9 @@ export const NOT_IN_BACKUP_MODELS: Readonly<Record<string, string>> = {
|
||||
|
||||
// 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:
|
||||
|
||||
@@ -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<FakePushAttempt, "createdAt"> }) => {
|
||||
pushAttempts.push({ ...data, createdAt: new Date() });
|
||||
return { id: `pa-${pushAttempts.length}` };
|
||||
async ({ data }: { data: Omit<FakeNotificationEvent, "createdAt"> }) => {
|
||||
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<void> {
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user