From b4748e3814e252ee429937c4276cf4818dc1f551 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 03:47:41 +0100 Subject: [PATCH] test(alerts): verify integrated incident history retry lifecycle The merged panel now consumes the real request error state, but its existing failure test only toggles a mock. Exercise the hook and Refresh button together so failed reads cannot masquerade as empty history and cached evidence survives a failed refresh. Change-source: pulse-maintainer --- .../AlertResourceIncidentsPanel.test.tsx | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/frontend-modern/src/features/alerts/__tests__/AlertResourceIncidentsPanel.test.tsx b/frontend-modern/src/features/alerts/__tests__/AlertResourceIncidentsPanel.test.tsx index 390e7ebd1..c9e6f2b7d 100644 --- a/frontend-modern/src/features/alerts/__tests__/AlertResourceIncidentsPanel.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/AlertResourceIncidentsPanel.test.tsx @@ -1,8 +1,12 @@ -import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; +import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library'; import { createSignal, type JSX } from 'solid-js'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AlertResourceIncidentsPanel } from '../AlertResourceIncidentsPanel'; +import { AlertsAPI } from '@/api/alerts'; +import type { Incident } from '@/types/api'; +import type { AlertHistoryState } from '../useAlertHistoryState'; +import { useAlertResourceIncidentsState } from '../useAlertResourceIncidentsState'; import { aiChatStore } from '@/stores/aiChat'; vi.mock('@solidjs/router', () => ({ @@ -22,6 +26,45 @@ describe('AlertResourceIncidentsPanel', () => { vi.restoreAllMocks(); }); + it('retries through the real hook and retains cached history after a refresh failure', async () => { + let state!: ReturnType; + const read = vi.spyOn(AlertsAPI, 'getIncidentsForResource'); + read.mockRejectedValueOnce(new Error('history unavailable')); + render(() => { + state = useAlertResourceIncidentsState(); + return ; + }); + + await state.openResourceIncidentPanel('resource-1', 'Resource', 'row-1'); + expect(screen.getByRole('alert')).toHaveTextContent('Use Refresh'); + expect(screen.queryByText('No incidents recorded for this resource yet.')).toBeNull(); + + let resolve!: (incidents: Incident[]) => void; + read.mockImplementationOnce(() => new Promise((done) => { resolve = done; })); + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })); + expect(screen.queryByRole('alert')).toBeNull(); + expect(screen.getByRole('button', { name: /Refresh/ })).toBeDisabled(); + resolve([{ + id: 'retained', alertType: 'connectivity', level: 'critical', + status: 'resolved', acknowledged: false, events: [], + openedAt: '2026-09-08T00:00:00Z', message: 'Retained connection incident', + } as Incident]); + await waitFor(() => expect(screen.getByText('Retained connection incident')).toBeVisible()); + expect(read).toHaveBeenLastCalledWith('resource-1', 10); + + read.mockRejectedValueOnce(new Error('refresh unavailable')); + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })); + await waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent('Use Refresh')); + expect(screen.getByText('Retained connection incident')).toBeVisible(); + expect(screen.queryByText('No incidents recorded for this resource yet.')).toBeNull(); + + read.mockResolvedValueOnce([]); + fireEvent.click(screen.getByRole('button', { name: 'Refresh' })); + await waitFor(() => expect(screen.getByText('No incidents recorded for this resource yet.')).toBeVisible()); + expect(screen.queryByRole('alert')).toBeNull(); + expect(screen.queryByText('Retained connection incident')).toBeNull(); + }); + it('shows persistent read failure without claiming an empty history', () => { const [failed, setFailed] = createSignal(true); render(() => (