diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/1440.png b/docs/qualification/release-v6.4-diagnosis-ordering/1440.png new file mode 100644 index 000000000..11ef19b88 Binary files /dev/null and b/docs/qualification/release-v6.4-diagnosis-ordering/1440.png differ diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/390.png b/docs/qualification/release-v6.4-diagnosis-ordering/390.png new file mode 100644 index 000000000..7619f4904 Binary files /dev/null and b/docs/qualification/release-v6.4-diagnosis-ordering/390.png differ diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/900.png b/docs/qualification/release-v6.4-diagnosis-ordering/900.png new file mode 100644 index 000000000..ba9a1e0e4 Binary files /dev/null and b/docs/qualification/release-v6.4-diagnosis-ordering/900.png differ diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/README.md b/docs/qualification/release-v6.4-diagnosis-ordering/README.md new file mode 100644 index 000000000..394c92c6a --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/README.md @@ -0,0 +1,43 @@ +# Release-line Overview diagnosis ordering regression + +Exact runtime parent: `101bae33963ee65a5a83bc229d081b43825de889`. +Backport source: `495562ef663e473889490a774a80cf8d04405732` (main). +Regression provenance: `32e620a6c36b4ce08729de747cb232dfabf5c1ba` +introduced the asynchronous delivery diagnosis projection on 26 August 2026; +it is an ancestor of this parent and v6.4.1. The unversioned completion path +is also present in v6.4.3-rc.1. This repairs an existing candidate surface, +not new product scope (RELEASE_PROMOTION_POLICY, Release Train rules 2/4). + +The main runtime patch and its two regression tests apply without adaptation. +With only those tests and the browser fixture added to the exact parent, +2 tests fail and 11 pass: overlapping reads replace the current disabled +warning, and an obsolete response repopulates an empty active set. Chromium +also fails the retained-warning assertion (0 versus 1) at its first viewport. +These are local release-line reproductions, not inherited main receipts. + +With the six-line runtime patch, all 13 tests pass. Another 25 adjacent +presentation and delivery-action tests pass. TypeScript (`tsc --noEmit`) exits +0. No full repository suite was run for this bounded UI backport. + +Commands: + +- `npm --prefix frontend-modern test -- src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx src/features/alerts/__tests__/useAlertOverviewState.test.tsx` +- `npm --prefix frontend-modern test -- src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts` +- `npm --prefix frontend-modern run type-check` +- `pulse-heavy-run -- node scripts/check-alert-diagnosis-ordering.mjs` + +The fixture uses the real Overview component and synthetic API responses on +loopback only. It does not exercise an installed backend, actual delivery, +recipient receipt, or production routing. Latest-started ownership prevents +superseded successful reads from updating state, including empty-set +invalidation. Failed refreshes still retain the previous snapshot; no new +freshness claim or indicator is added. + +This does not resolve warning-projection ordering, SQLite crashes, paid +convergence, recipient qualification or the release HOLD. No public release +or issue-resolution claim follows from this component evidence. + +Patched Chromium passes at 1440, 900 and 390 pixels: warning retained, stale +dispatch absent, new alert present, text within viewport and no page errors. +Desktop and phone screenshots were visually inspected: warning and controls +remain visible. Receipt binds the runtime file hash to the exact parent. diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/adjacent-tests.txt b/docs/qualification/release-v6.4-diagnosis-ordering/adjacent-tests.txt new file mode 100644 index 000000000..d08483421 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/adjacent-tests.txt @@ -0,0 +1,14 @@ + +> pulse-modern@1.0.0 test +> vitest run src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts + + + RUN v3.2.7 /var/lib/pulse-maintainer/team-worktrees/20260906T163509Z-release-line/pulse@release_v6.4/frontend-modern + + ✓ src/features/alerts/__tests__/deliveryDiagnosisPresentation.test.ts (16 tests) 137ms + ✓ src/features/alerts/__tests__/OverviewTab.deliveryactions.test.tsx (9 tests) 358ms + + Test Files 2 passed (2) + Tests 25 passed (25) + Start at 17:38:35 + Duration 2.35s (transform 1.13s, setup 212ms, collect 1.21s, tests 495ms, environment 727ms, prepare 184ms) diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/fixed-browser.txt b/docs/qualification/release-v6.4-diagnosis-ordering/fixed-browser.txt new file mode 100644 index 000000000..c47cb4689 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/fixed-browser.txt @@ -0,0 +1 @@ +{"result":"passed","viewports":[1440,900,390],"scope":"Real Overview and Chromium; scripted diagnoses, not installed delivery or receipt"} diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/fixed-tests.txt b/docs/qualification/release-v6.4-diagnosis-ordering/fixed-tests.txt new file mode 100644 index 000000000..cd55867b9 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/fixed-tests.txt @@ -0,0 +1,14 @@ + +> pulse-modern@1.0.0 test +> vitest run src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx src/features/alerts/__tests__/useAlertOverviewState.test.tsx + + + RUN v3.2.7 /var/lib/pulse-maintainer/team-worktrees/20260906T163509Z-release-line/pulse@release_v6.4/frontend-modern + + ✓ src/features/alerts/__tests__/useAlertOverviewState.test.tsx (8 tests) 41ms + ✓ src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx (5 tests) 156ms + + Test Files 2 passed (2) + Tests 13 passed (13) + Start at 17:38:05 + Duration 1.84s (transform 964ms, setup 109ms, collect 1.18s, tests 197ms, environment 658ms, prepare 123ms) diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/parent-browser.txt b/docs/qualification/release-v6.4-diagnosis-ordering/parent-browser.txt new file mode 100644 index 000000000..695ccd2e2 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/parent-browser.txt @@ -0,0 +1,18 @@ +node:internal/modules/run_main:107 + triggerUncaughtException( + ^ + +AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + +0 !== 1 + + at file:///var/lib/pulse-maintainer/team-worktrees/20260906T163509Z-release-line/pulse@release_v6.4/scripts/check-alert-diagnosis-ordering.mjs:103:12 { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' +} + +Node.js v24.20.0 diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/parent-tests.txt b/docs/qualification/release-v6.4-diagnosis-ordering/parent-tests.txt new file mode 100644 index 000000000..eddb46317 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/parent-tests.txt @@ -0,0 +1,80 @@ + +> pulse-modern@1.0.0 test +> vitest run src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx src/features/alerts/__tests__/useAlertOverviewState.test.tsx + + + RUN v3.2.7 /var/lib/pulse-maintainer/team-worktrees/20260906T163509Z-release-line/pulse@release_v6.4/frontend-modern + + ❯ src/features/alerts/__tests__/useAlertOverviewState.test.tsx (8 tests | 1 failed) 47ms + × useAlertOverviewState > invalidates pending diagnosis reads when the active set becomes empty 13ms + → expected { a1: { alertIdentifier: 'a1', …(1) } } to deeply equal {} + ✓ useAlertOverviewState > owns overview stats, filtering, and acknowledge flows outside the tab shell 4ms + ✓ useAlertOverviewState > keeps a stable id order for alerts sharing a startTime (#1218) 1ms + ✓ useAlertOverviewState > counts alerts with missing startTime in total24h 1ms + ✓ useAlertOverviewState > groups only explicit shared-system correlations across resources 1ms + ✓ useAlertOverviewState > groups explicit infrastructure synthesis without hiding the supporting detector 1ms + ✓ useAlertOverviewState > does not infer correlation by truncating resource paths 1ms + ✓ useAlertOverviewState > still groups multiple detector signals for the exact same resource 1ms + ❯ src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx (5 tests | 1 failed) 167ms + ✓ OverviewTab delivery status line > renders held-notification status from the bulk diagnosis endpoint 95ms + ✓ OverviewTab delivery status line > does not turn dispatch evidence into receipt for 'ready' 16ms + ✓ OverviewTab delivery status line > does not turn dispatch evidence into receipt for 'cooldown' 17ms + × OverviewTab delivery status line > ignores an older diagnosis response after the active alert set changes 24ms + → expected to be null + ✓ OverviewTab delivery status line > renders no delivery line when the diagnosis fetch fails 10ms + +⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯ + + FAIL src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx > OverviewTab delivery status line > ignores an older diagnosis response after the active alert set changes +AssertionError: expected to be null + +- Expected: +null + ++ Received: + + Dispatch requested 11:15 AM + + + ❯ src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx:145:56 + 143| finishOlder([makeDiagnosis('a1', { lastNotified: '2026-08-26T10:15… + 144| await Promise.resolve(); + 145| expect(screen.queryByText(/^Dispatch requested /)).toBeNull(); + | ^ + 146| expect(screen.getByText('Notifications are turned off')).toBeTruth… + 147| }); + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ + + FAIL src/features/alerts/__tests__/useAlertOverviewState.test.tsx > useAlertOverviewState > invalidates pending diagnosis reads when the active set becomes empty +AssertionError: expected { a1: { alertIdentifier: 'a1', …(1) } } to deeply equal {} + +- Expected ++ Received + +- {} ++ { ++ "a1": { ++ "alertIdentifier": "a1", ++ "reason": "ready", ++ }, ++ } + + ❯ src/features/alerts/__tests__/useAlertOverviewState.test.tsx:85:40 + 83| finish([{ alertIdentifier: 'a1', reason: 'ready' } as AlertDeliver… + 84| await Promise.resolve(); + 85| expect(result.deliveryDiagnoses()).toEqual({}); + | ^ + 86| }); + 87| + +⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯ + + + Test Files 2 failed (2) + Tests 2 failed | 11 passed (13) + Start at 17:37:40 + Duration 1.88s (transform 1.03s, setup 143ms, collect 1.24s, tests 214ms, environment 607ms, prepare 329ms) diff --git a/docs/qualification/release-v6.4-diagnosis-ordering/typecheck.txt b/docs/qualification/release-v6.4-diagnosis-ordering/typecheck.txt new file mode 100644 index 000000000..4b83befb6 --- /dev/null +++ b/docs/qualification/release-v6.4-diagnosis-ordering/typecheck.txt @@ -0,0 +1,3 @@ + +> pulse-modern@1.0.0 type-check +> tsc --noEmit diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 3a9a796dc..17c361dd9 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -15,6 +15,15 @@ ## Purpose +Overview delivery diagnoses use latest-started refresh ownership. Older bulk +responses cannot overwrite newer card notification status, and an empty active +alert set invalidates outstanding reads. Disposal also prevents updates. Failed +refreshes retain the existing snapshot; this ordering repair does not add a +freshness indicator or establish recipient receipt. Verify response overlap in +`OverviewTab.deliverystatus.test.tsx`, empty-set invalidation in +`useAlertOverviewState.test.tsx`, and rendered ordering at three widths using +`scripts/check-alert-diagnosis-ordering.mjs`. + Confirmed canonical metric recovery publishes the clearing evaluation's value, observation time, and resolved metric wording in the snapshot consumed by recent-resolution reads and notification callbacks. Clone that snapshot before diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index de5dd45da..f721eebeb 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -20,6 +20,15 @@ ## Purpose +Overview delivery diagnoses use latest-started refresh ownership. Older bulk +responses cannot overwrite newer card notification status, and an empty active +alert set invalidates outstanding reads. Disposal also prevents updates. Failed +refreshes retain the existing snapshot; this ordering repair does not add a +freshness indicator or establish recipient receipt. Verify response overlap in +`OverviewTab.deliverystatus.test.tsx`, empty-set invalidation in +`useAlertOverviewState.test.tsx`, and rendered ordering at three widths using +`scripts/check-alert-diagnosis-ordering.mjs`. + Proxmox backup presentation treats every manifestless PBS artifact as non-recoverable. It renders the artifact as `Running` when current writer visibility is absent or a matching writer is active, and as danger-tone diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index f36da173a..11dde8f7a 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,13 +1,13 @@ { "version": 1, - "base_sha": "2d550da942c6f0a7e7896e7aca86493983f90b84", - "verified_at": "2026-09-06T15:54:59.817913Z", + "base_sha": "101bae33963ee65a5a83bc229d081b43825de889", + "verified_at": "2026-09-06T16:42:39.585210Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts" + "frontend-modern/src/features/alerts/useAlertOverviewState.ts" ], "content_sha256": { - "frontend-modern/src/features/alerts/deliveryDiagnosisPresentation.ts": "bc3eadf8f790517b377430a42eb67e8fdfcadb53bb13b87cd971d6a9ab91d607" + "frontend-modern/src/features/alerts/useAlertOverviewState.ts": "64d0b891e7ad228e8590da859dc25e825b6164c8cf76a01983a219d6cd079b23" }, "routes": [ "/qualification (loopback synthetic Overview, not production routing)" @@ -27,11 +27,11 @@ } ], "states": [ - "Ready and cooldown show Dispatch requested; cooldown says next eligible; missing timestamp shows Notification pending. Scripted diagnoses, not recipient receipt.", - "390px screenshot visually inspected: longer status wraps within card and controls remain visible." + "Newer notifications-disabled response remains visible after older dispatch response completes. Synthetic diagnoses, not recipient receipt.", + "Desktop and phone screenshots visually inspected: current warning and card controls remain visible within viewport." ], "interactions": [ - "pulse-heavy-run -- node scripts/check-alert-dispatch-copy.mjs passed at all three widths; two dispatch labels, no Notified labels, one pending label, text fits viewport, no page errors.", - "Evidence: /var/lib/pulse-maintainer/queue/staging/20260906T155036Z-release-line/browser-dispatch.log and browser-dispatch/*.png." + "pulse-heavy-run -- node scripts/check-alert-diagnosis-ordering.mjs passed at 1440, 900 and 390 pixels: retained warning, absent stale dispatch, new alert present, text within viewport, no page errors.", + "Exact-parent failing and patched passing evidence: docs/qualification/release-v6.4-diagnosis-ordering/." ] } diff --git a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx index 631ae1590..5dcaaaeb7 100644 --- a/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/OverviewTab.deliverystatus.test.tsx @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createSignal } from 'solid-js'; import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n'; import type { Alert, AlertDeliveryDiagnosis } from '@/types/api'; @@ -124,6 +125,27 @@ describe('OverviewTab delivery status line', () => { if (state.reason === 'cooldown') expect(screen.getByText(/next eligible/)).toBeTruthy(); }); + it('ignores an older diagnosis response after the active alert set changes', async () => { + let finishOlder!: (value: AlertDeliveryDiagnosis[]) => void; + getDeliveryDiagnoses.mockReturnValueOnce( + new Promise((resolve) => { + finishOlder = resolve; + }), + ); + getDeliveryDiagnoses.mockResolvedValueOnce([ + makeDiagnosis('a1', { status: 'suppressed', reason: 'notifications_disabled' }), + ]); + const [alerts, setAlerts] = createSignal>({ a1: makeAlert('a1') }); + render(() => ); + await waitFor(() => expect(getDeliveryDiagnoses).toHaveBeenCalledTimes(1)); + setAlerts({ a1: makeAlert('a1'), a2: makeAlert('a2') }); + await waitFor(() => expect(screen.getByText('Notifications are turned off')).toBeTruthy()); + finishOlder([makeDiagnosis('a1', { lastNotified: '2026-08-26T10:15:00Z' })]); + await Promise.resolve(); + expect(screen.queryByText(/^Dispatch requested /)).toBeNull(); + expect(screen.getByText('Notifications are turned off')).toBeTruthy(); + }); + it('renders no delivery line when the diagnosis fetch fails', async () => { const activeAlerts: Record = { a1: makeAlert('a1') }; getDeliveryDiagnoses.mockRejectedValue(new Error('boom')); diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx index edc4f4569..2b3a96ddc 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertOverviewState.test.tsx @@ -4,12 +4,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AlertsAPI } from '@/api/alerts'; import { notificationStore } from '@/stores/notifications'; -import type { Alert } from '@/types/api'; +import type { Alert, AlertDeliveryDiagnosis } from '@/types/api'; import { useAlertOverviewState } from '../useAlertOverviewState'; vi.mock('@/api/alerts', () => ({ AlertsAPI: { + getDeliveryDiagnoses: vi.fn(), acknowledge: vi.fn(), bulkAcknowledge: vi.fn(), unacknowledge: vi.fn(), @@ -47,6 +48,7 @@ describe('useAlertOverviewState', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-03-22T12:00:00Z')); + vi.mocked(AlertsAPI.getDeliveryDiagnoses).mockReset().mockResolvedValue([]); vi.mocked(AlertsAPI.acknowledge).mockReset(); vi.mocked(AlertsAPI.unacknowledge).mockReset(); vi.mocked(AlertsAPI.bulkAcknowledge).mockReset(); @@ -58,6 +60,31 @@ describe('useAlertOverviewState', () => { vi.useRealTimers(); }); + it('invalidates pending diagnosis reads when the active set becomes empty', async () => { + let finish!: (value: AlertDeliveryDiagnosis[]) => void; + vi.mocked(AlertsAPI.getDeliveryDiagnoses).mockReturnValueOnce( + new Promise((resolve) => { + finish = resolve; + }), + ); + const [activeAlerts, setActiveAlerts] = createSignal>({ + a1: makeAlert('a1', new Date().toISOString()), + }); + const { result } = renderHook(() => + useAlertOverviewState({ + activeAlerts, + overrides: () => [], + showAcknowledged: () => true, + updateAlert: vi.fn(), + }), + ); + expect(AlertsAPI.getDeliveryDiagnoses).toHaveBeenCalledOnce(); + setActiveAlerts({}); + finish([{ alertIdentifier: 'a1', reason: 'ready' } as AlertDeliveryDiagnosis]); + await Promise.resolve(); + expect(result.deliveryDiagnoses()).toEqual({}); + }); + it('owns overview stats, filtering, and acknowledge flows outside the tab shell', async () => { const now = Date.now(); const [activeAlerts] = createSignal>({ diff --git a/frontend-modern/src/features/alerts/useAlertOverviewState.ts b/frontend-modern/src/features/alerts/useAlertOverviewState.ts index 650a92063..377bb479e 100644 --- a/frontend-modern/src/features/alerts/useAlertOverviewState.ts +++ b/frontend-modern/src/features/alerts/useAlertOverviewState.ts @@ -68,17 +68,21 @@ export function useAlertOverviewState(props: UseAlertOverviewStateProps) { Record >({}); let diagnosisStateDisposed = false; + let diagnosisRequestVersion = 0; onCleanup(() => { diagnosisStateDisposed = true; }); const refreshDeliveryDiagnoses = async () => { + // A slower previous refresh must not replace a newer notification state. + // Increment even for an empty alert set to invalidate outstanding requests. + const requestVersion = ++diagnosisRequestVersion; if (activeAlerts().length === 0) { setDeliveryDiagnoses({}); return; } try { const list = await AlertsAPI.getDeliveryDiagnoses(); - if (diagnosisStateDisposed) return; + if (diagnosisStateDisposed || requestVersion !== diagnosisRequestVersion) return; const next: Record = {}; for (const diagnosis of list) { next[diagnosis.alertIdentifier || diagnosis.alertId] = diagnosis; diff --git a/scripts/check-alert-diagnosis-ordering.mjs b/scripts/check-alert-diagnosis-ordering.mjs new file mode 100644 index 000000000..45b4e30f4 --- /dev/null +++ b/scripts/check-alert-diagnosis-ordering.mjs @@ -0,0 +1,146 @@ +// Isolated real-browser component qualification; no installed backend or delivery claim. +import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js"; +import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs"; +import { chromium } from "@playwright/test"; +import { resolve } from "node:path"; +import { mkdirSync } from "node:fs"; +import assert from "node:assert/strict"; +const root = resolve("frontend-modern"); +process.chdir(root); +const fixture = ` +import { createSignal } from 'solid-js'; +import { render } from 'solid-js/web'; +import { Router, Route } from '@solidjs/router'; +import { AlertsAPI } from '/src/api/alerts'; +import { NotificationsAPI } from '/src/api/notifications'; +import { OverviewTab } from '/src/features/alerts/OverviewTab'; +import '/src/index.css'; +let finishOlder; +let requests = 0; +AlertsAPI.getDeliveryDiagnoses = () => { + requests++; + if (requests === 1) return new Promise(resolve => { finishOlder = resolve; }); + return Promise.resolve([{alertIdentifier:'a1', alertId:'a1', status:'suppressed', + reason:'notifications_disabled', message:'Notifications disabled by current configuration'}]); +}; +window.finishOlder = () => finishOlder([{alertIdentifier:'a1', alertId:'a1', + status:'would_send',reason:'ready',lastNotified:'2026-08-26T10:15:00Z'}]); +window.requestCount = () => requests; +AlertsAPI.getEvents = async () => []; +NotificationsAPI.getHealth = async () => ({queue:{status:'healthy'}}); +const alert = id => ({id,resourceId:id,resourceName:'VM '+id,type:'cpu',level:'warning', +message:'High CPU on '+id,startTime:new Date().toISOString(),acknowledged:false,node:'node1'}); +function Fixture() { +const [alerts, setAlerts] = createSignal({a1:alert('a1')}); +return
+{}} showQuickTip={()=>false} dismissQuickTip={()=>{}} showAcknowledged={()=>true} +setShowAcknowledged={()=>{}} alertsDisabled={()=>false}/>
; } +render(()=>,document.getElementById('root')); +`; +const server = await createServer({ + root, + configFile: false, + optimizeDeps: { + noDiscovery: true, + entries: [], + esbuildOptions: { target: "esnext" }, + }, + esbuild: { target: "esnext" }, + plugins: [ + solid(), + { + name: "dispatch-fixture", + configureServer(s) { + s.middlewares.use((req, res, next) => { + if (req.url === "/qualification") { + res.setHeader("Content-Type", "text/html"); + res.end( + '
', + ); + } else next(); + }); + }, + resolveId(id) { + if (id === "/dispatch-fixture.tsx") return id; + }, + load(id) { + if (id === "/dispatch-fixture.tsx") return fixture; + }, + }, + ], + resolve: { alias: { "@": resolve(root, "src") } }, + server: { host: "127.0.0.1", port: 5199, strictPort: true }, +}); +let browser; +try { + await server.listen(); + browser = await chromium.launch({ headless: true }); + mkdirSync("/tmp/pulse-alert-diagnosis-ordering", { recursive: true }); + for (const width of [1440, 900, 390]) { + const page = await browser.newPage({ viewport: { width, height: 1000 } }); + const errors = []; + page.on("pageerror", (e) => { + errors.push(e.message); + console.error(e.message); + }); + page.on("console", (m) => { + if (m.type() === "error") console.error(m.text()); + }); + await page.route("http://127.0.0.1:5199/api/**", (route) => + route.fulfill({ json: [] }), + ); + await page.goto("http://127.0.0.1:5199/qualification"); + await page.waitForFunction(() => window.requestCount?.() === 1); + await page.getByRole("button", { name: "Add alert", exact: true }).click(); + await page + .getByText("Notifications are turned off", { exact: true }) + .waitFor(); + await page.evaluate(async () => { + window.finishOlder(); + await Promise.resolve(); + }); + assert.equal( + await page + .getByText("Notifications are turned off", { exact: true }) + .count(), + 1, + ); + assert.equal(await page.getByText(/^Dispatch requested /).count(), 0); + assert.equal( + await page.getByText("High CPU on a2", { exact: true }).count(), + 1, + ); + const label = page.getByText("Notifications are turned off", { + exact: true, + }); + assert.equal( + await label.evaluate((el) => { + const range = document.createRange(); + range.selectNodeContents(el); + return [...range.getClientRects()].every( + (b) => b.left >= 0 && b.right <= innerWidth, + ); + }), + true, + "current status must fit viewport", + ); + assert.deepEqual(errors, []); + await page.screenshot({ + path: "/tmp/pulse-alert-diagnosis-ordering/" + width + ".png", + fullPage: true, + }); + await page.close(); + } + console.log( + JSON.stringify({ + result: "passed", + viewports: [1440, 900, 390], + scope: + "Real Overview and Chromium; scripted diagnoses, not installed delivery or receipt", + }), + ); +} finally { + await browser?.close(); + await server.close(); +}