From e2a125ac348c4215bd38de5fac57d836ba6d14b1 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:27:24 +0100 Subject: [PATCH] test(alerts): retain delivery-log ordering defect reproductions Mount and retry reads can overlap and overwrite newer delivery evidence. Preserve three explicit expected-failure invariants so the pending repair has deterministic reproduction, plus proof that held events do not delay the primary read. Mock held-event requests rather than leaking network calls from hook tests. Contract-Neutral: tests only; no runtime or user-visible behaviour changed Change-source: pulse-maintainer --- .../useNotificationDeliveryLog.test.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/frontend-modern/src/features/alerts/__tests__/useNotificationDeliveryLog.test.tsx b/frontend-modern/src/features/alerts/__tests__/useNotificationDeliveryLog.test.tsx index e715732af..d7b110d3d 100644 --- a/frontend-modern/src/features/alerts/__tests__/useNotificationDeliveryLog.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useNotificationDeliveryLog.test.tsx @@ -1,6 +1,8 @@ import { createRoot } from 'solid-js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { AlertsAPI } from '@/api/alerts'; +import type { NotificationDeliveryLog } from '@/api/notifications'; import { NotificationsAPI } from '@/api/notifications'; import { useNotificationDeliveryLog } from '../useNotificationDeliveryLog'; @@ -9,9 +11,31 @@ vi.mock('@/api/notifications', () => ({ NotificationsAPI: { getDeliveryLog: vi.fn() }, })); +vi.mock('@/api/alerts', () => ({ + AlertsAPI: { getEvents: vi.fn() }, +})); + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason: Error) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +const emptyLog: NotificationDeliveryLog = { + entries: [], + windowDays: 30, + completedRetentionDays: 7, + deadLetterRetentionDays: 30, +}; + describe('useNotificationDeliveryLog', () => { beforeEach(() => { vi.mocked(NotificationsAPI.getDeliveryLog).mockReset(); + vi.mocked(AlertsAPI.getEvents).mockReset().mockResolvedValue([]); }); it('exposes the loaded log and clears the unavailable flag', () => @@ -71,4 +95,84 @@ describe('useNotificationDeliveryLog', () => { expect(state.deliveryLog()?.entries).toHaveLength(0); dispose(); })); + + it('does not wait for held events before completing the delivery read', () => + createRoot(async (dispose) => { + const held = deferred>>(); + vi.mocked(AlertsAPI.getEvents).mockReturnValue(held.promise); + vi.mocked(NotificationsAPI.getDeliveryLog).mockResolvedValue(emptyLog); + const state = useNotificationDeliveryLog(); + try { + await state.loadDeliveryLog(); + expect(state.deliveryLog()).toEqual(emptyLog); + expect(state.refreshingDeliveryLog()).toBe(false); + expect(state.heldEvents()).toEqual([]); + } finally { + held.resolve([]); + dispose(); + } + })); + + // Known defect: mount and queue-action refreshes can overlap. Keep the + // desired invariant executable until the governed runtime repair lands; + // Vitest fails these tests if the invariant starts passing unexpectedly. + it.fails('keeps the newest successful read when an older read fails', () => + createRoot(async (dispose) => { + const older = deferred(); + vi.mocked(NotificationsAPI.getDeliveryLog) + .mockReturnValueOnce(older.promise) + .mockResolvedValueOnce(emptyLog); + const state = useNotificationDeliveryLog(); + try { + const first = state.loadDeliveryLog(); + await state.loadDeliveryLog(); + older.reject(new Error('old read failed')); + await first; + expect(state.deliveryLogUnavailable()).toBe(false); + expect(state.deliveryLog()).toEqual(emptyLog); + } finally { + dispose(); + } + })); + + it.fails('does not replace a newer unavailable result with an older success', () => + createRoot(async (dispose) => { + const older = deferred(); + vi.mocked(NotificationsAPI.getDeliveryLog) + .mockReturnValueOnce(older.promise) + .mockRejectedValueOnce(new Error('new read failed')); + const state = useNotificationDeliveryLog(); + try { + const first = state.loadDeliveryLog(); + await state.loadDeliveryLog(); + older.resolve(emptyLog); + await first; + expect(state.deliveryLogUnavailable()).toBe(true); + expect(state.deliveryLog()).toBeNull(); + } finally { + dispose(); + } + })); + + it.fails('keeps refreshing true while the newest read remains pending', () => + createRoot(async (dispose) => { + const older = deferred(); + const newer = deferred(); + vi.mocked(NotificationsAPI.getDeliveryLog) + .mockReturnValueOnce(older.promise) + .mockReturnValueOnce(newer.promise); + const state = useNotificationDeliveryLog(); + const first = state.loadDeliveryLog(); + const second = state.loadDeliveryLog(); + try { + older.resolve(emptyLog); + await first; + expect(state.refreshingDeliveryLog()).toBe(true); + } finally { + newer.resolve(emptyLog); + await second; + dispose(); + } + })); + });