mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
Fix stale notification health ownership with browser regression proof
Keep overlapping success, failure and loading completion owned by the latest started read. Retain caller and queue-action controls, subsystem contracts and exact-source scripted Chromium evidence. Installed delivery and release qualification remain separate. Change-source: pulse-maintainer
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,28 @@
|
||||
# Delivery health request ordering — 5 September 2026
|
||||
|
||||
Run from repository root after root and frontend `npm ci --ignore-scripts`:
|
||||
|
||||
pulse-heavy-run -- node scripts/check-delivery-health-ordering.mjs
|
||||
|
||||
Twelve real Chromium cases passed at 1440×900 and 390×900. The fixture imports
|
||||
the production destinations caller, health hook, warning card and CSS; only API
|
||||
responses, configuration props and confirmation acceptance are scripted.
|
||||
It does not serve the complete application or exercise an installed backend.
|
||||
The temporary Vite fixture is never part of the product bundle.
|
||||
|
||||
Four success/error orderings per width preserve latest attention and rendered
|
||||
state. Two action cases per width exercise retry/dismissal overlap: an older
|
||||
completion cannot clear loading while the post-action read remains pending,
|
||||
and the latest healthy response clears attention. All 19 focused hook, caller
|
||||
and card tests pass; reverting only the runtime hook produces nine failures in
|
||||
the 16 hook/caller tests. TypeScript and changed-file ESLint pass.
|
||||
|
||||
The first browser attempt failed before UI verification because the isolated
|
||||
Vite configuration omitted the repository's esnext dependency target. The
|
||||
retained runner corrects that and uses the frontend working directory for CSS.
|
||||
`browser-result.json` is the successful rerun's output; hashes bind the runner
|
||||
and runtime source. Representative screenshots show actual fixture rendering.
|
||||
Warning titles appear pale in this isolated CSS context; this receipt qualifies
|
||||
ordering, not whole-app contrast or accessibility. Full-shell visual review,
|
||||
installed delivery, release-line applicability and exact-candidate integration
|
||||
remain separate qualification work. No public release is claimed.
|
||||
@@ -0,0 +1 @@
|
||||
{"result":"passed","cases":12,"viewports":[1440,390],"scope":"real Chromium, real caller/hook/card, scripted API; not installed application or notification receipt"}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"scripts/check-delivery-health-ordering.mjs": "69f2c34c0fb0ecf1e67f5e5c85051f63d943314d975d6eadb8f37363622dd9c9",
|
||||
"frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts": "1eadf6df30b4f1130f868b8f139561ea60fdb87e1823deeeaaf2f8fbc699a00c"
|
||||
}
|
||||
@@ -2579,3 +2579,19 @@ reasoning and real remediation in
|
||||
`docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md`. The repeatable browser
|
||||
proof is `scripts/check-patrol-assistant-journey.mjs`. A passing scripted
|
||||
response does not establish a useful customer outcome or model qualification.
|
||||
|
||||
### Delivery health requests have latest-started ownership
|
||||
|
||||
Overlapping mount, configuration Retry and post-queue-action reads must not
|
||||
allow an older completion to replace newer delivery health. Success, failure,
|
||||
first-load completion and the refreshing flag belong only to the most recently
|
||||
started health request. A stale healthy response cannot hide degraded attention;
|
||||
a stale error cannot invent unavailability after recovery. Queue actions still
|
||||
refresh from the server, not from their affected count. This changes no provider
|
||||
acceptance, incident lifecycle or delivery guarantee.
|
||||
|
||||
The hook and destinations caller regressions in
|
||||
`useNotificationDeliveryHealth.test.tsx` and
|
||||
`useAlertDestinationsTabState.test.tsx` pin ordering and loading ownership.
|
||||
`scripts/check-delivery-health-ordering.mjs` exercises the real caller and card
|
||||
in Chromium with scripted API completions; it is not installed delivery proof.
|
||||
|
||||
@@ -7108,3 +7108,15 @@ reasoning and real remediation in
|
||||
`docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md`. The repeatable browser
|
||||
proof is `scripts/check-patrol-assistant-journey.mjs`. A passing scripted
|
||||
response does not establish a useful customer outcome or model qualification.
|
||||
|
||||
### Alert health attention preserves asynchronous ownership
|
||||
|
||||
The existing delivery-health card and shared buttons consume only the latest
|
||||
started health read's state. Configuration Retry can overlap a disabled card
|
||||
refresh; disabling that button is not a concurrency guard. Older completions
|
||||
must neither clear the latest request's busy flag nor replace its attention or
|
||||
unavailable presentation. Existing danger tone, accessible alert role, labels,
|
||||
confirmation and wrapping controls remain unchanged; no new primitive is added.
|
||||
The focused hook/caller tests and `scripts/check-delivery-health-ordering.mjs`
|
||||
cover this dependency at desktop and narrow widths using scripted health and
|
||||
queue-action responses, without claiming backend notification delivery.
|
||||
|
||||
@@ -1,53 +1,34 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "75976c60be5e156be46fca344a02b7df5f573416",
|
||||
"verified_at": "2026-09-05T17:45:39.915245Z",
|
||||
"base_sha": "2a833eccdfecf7248eed63eabf5b9184a9fa2390",
|
||||
"verified_at": "2026-09-05T23:57:09.414504Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts",
|
||||
"frontend-modern/src/components/AI/Chat/index.tsx",
|
||||
"frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx",
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
|
||||
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts",
|
||||
"frontend-modern/src/stores/aiChat.ts",
|
||||
"frontend-modern/src/utils/aiChatPresentation.ts"
|
||||
"frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/AI/Chat/hooks/useExplanationRequest.ts": "8606c79c11b0197ed5a55ed1d2ec83e0cd7dd0adda182937e5aad9144571cb17",
|
||||
"frontend-modern/src/components/AI/Chat/index.tsx": "6b1d6243d9a99d688229dea9e954ae6aa770a8b6e07e7f0ce75316b486090f09",
|
||||
"frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx": "f373ff21a1c54304d63b92498c39fa45f5ffb036a2b02e93de64fafbbf8ee52e",
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "f098cddd58823d2df7687f0db7774363fc9c81a7a8845a917e73415a7f5aae10",
|
||||
"frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts": "4152a3bec5412b4d744436128e8e038be349d9b6419fc58d3b7527908ffc982f",
|
||||
"frontend-modern/src/stores/aiChat.ts": "d0b1ef69bcb454e6f0ce17a157c98bdd34d68d90f607b76081e9cd1bd6778e4c",
|
||||
"frontend-modern/src/utils/aiChatPresentation.ts": "4b05f663110ec6c789f8ff050993a945e3e9d41c6e3fee4047500e979a48531d"
|
||||
"frontend-modern/src/features/alerts/useNotificationDeliveryHealth.ts": "1eadf6df30b4f1130f868b8f139561ea60fdb87e1823deeeaaf2f8fbc699a00c"
|
||||
},
|
||||
"routes": [
|
||||
"/patrol",
|
||||
"/alerts"
|
||||
"/qualification (isolated caller/card fixture, not application routing)"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 1000
|
||||
},
|
||||
{
|
||||
"width": 900,
|
||||
"height": 1000
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 1000
|
||||
"height": 900
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Current hot-dev frontend with mock monitoring backend. Scripted attention evidence and inference responses. Other non-GET requests blocked. No real model or infrastructure outcome qualification.",
|
||||
"Patrol selected item, expanded evidence, scoped explanation, streamed response, failed provider request and successful retry. Draft preserved across Escape and context-only reopen.",
|
||||
"Active alert secondary menu at desktop and narrow widths. Open, Escape dismissal, outside click dismissal, reopen, select explanation, menu closes and alert context sent. Screenshots inspected for menu placement, readable response, reachable composer and error controls."
|
||||
"Real Chromium with current useAlertDestinationsTabState, useNotificationDeliveryHealth and AlertDeliveryHealthCard; scripted API promises and queue actions. No installed backend or provider receipt.",
|
||||
"Older healthy/newer degraded, older degraded/newer healthy, older error/newer healthy, older healthy/newer error. Warning presence and rendered state remain owned by newer request. Desktop and narrow screenshots inspected; this is not a full-shell or accessibility audit."
|
||||
],
|
||||
"interactions": [
|
||||
"node scripts/check-patrol-assistant-journey.mjs",
|
||||
"Patrol at 1440/900/390: keyboard focus and Enter on Explain, exactly one selected-issue request, correct evidence/resource, autonomous false, no generic workflow starters, close/reopen preserves draft without inference.",
|
||||
"Alerts at 1440/390: More alert actions, Escape, reopen, outside click, reopen, Explain. Selected alert evidence and resource attached with autonomous false. No Patrol trigger or configuration mutation.",
|
||||
"Provider failure, Try again retains original handoff, reload does not resubmit. No page errors or unexpected non-GET requests."
|
||||
"pulse-heavy-run -- node scripts/check-delivery-health-ordering.mjs: 12 cases passed.",
|
||||
"Configuration Retry overlaps pending mount health. Resolve newer then older and compare rendered main text and alert presence.",
|
||||
"Retry retained deliveries and Dismiss retained failures each start post-action refresh while another read is pending. Older completion leaves loading true; latest healthy response clears warning. Confirmations and API responses are scripted."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -81,6 +81,40 @@ const buildAppriseConfig = (): UIAppriseConfig => ({
|
||||
});
|
||||
|
||||
describe('useAlertDestinationsTabState', () => {
|
||||
it('preserves degraded configuration Retry health when the older mount request finishes', async () => {
|
||||
let finishMount!: (health: Awaited<ReturnType<typeof NotificationsAPI.getHealth>>) => void;
|
||||
const healthy = { queue: { status: 'healthy', attentionRequired: 0 } } as Awaited<
|
||||
ReturnType<typeof NotificationsAPI.getHealth>
|
||||
>;
|
||||
vi.mocked(NotificationsAPI.getWebhooks).mockResolvedValue([]);
|
||||
vi.mocked(NotificationsAPI.getDeliveryLog).mockResolvedValue({ entries: [] } as never);
|
||||
vi.mocked(NotificationsAPI.getHealth)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
finishMount = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({ ...healthy, queue: { ...healthy.queue, status: 'degraded' } });
|
||||
const [appriseConfig, setAppriseConfig] = createSignal(buildAppriseConfig());
|
||||
const { result } = renderHook(() =>
|
||||
useAlertDestinationsTabState({
|
||||
appriseConfig,
|
||||
setAppriseConfig,
|
||||
configLoadError: () => null,
|
||||
emailConfig: () => buildEmailConfig(),
|
||||
isLoadingDestinations: () => false,
|
||||
isRetrying: () => false,
|
||||
onRetryLoad: vi.fn(),
|
||||
}),
|
||||
);
|
||||
result.handleRetry();
|
||||
await waitFor(() => expect(result.deliveryNeedsAttention()).toBe(true));
|
||||
finishMount(healthy);
|
||||
await Promise.resolve();
|
||||
expect(result.deliveryHealth()?.queue.status).toBe('degraded');
|
||||
expect(result.deliveryNeedsAttention()).toBe(true);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(AlertsAPI.getEvents).mockReset();
|
||||
vi.mocked(AlertsAPI.getEvents).mockResolvedValue([]);
|
||||
|
||||
+108
-1
@@ -6,7 +6,11 @@ import { NotificationsAPI } from '@/api/notifications';
|
||||
import { useNotificationDeliveryHealth } from '../useNotificationDeliveryHealth';
|
||||
|
||||
vi.mock('@/api/notifications', () => ({
|
||||
NotificationsAPI: { getHealth: vi.fn() },
|
||||
NotificationsAPI: {
|
||||
getHealth: vi.fn(),
|
||||
dismissTerminalFailures: vi.fn(),
|
||||
retryTerminalFailures: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const healthWith = (status: string) => ({ queue: { status, failed: 3, deadLetter: 1 } }) as never;
|
||||
@@ -60,4 +64,107 @@ describe('useNotificationDeliveryHealth', () => {
|
||||
expect(state.deliveryNeedsAttention()).toBe(true);
|
||||
dispose();
|
||||
}));
|
||||
it.each([
|
||||
['healthy', 'degraded'],
|
||||
['degraded', 'healthy'],
|
||||
['error', 'healthy'],
|
||||
['healthy', 'error'],
|
||||
])('ignores older %s completion after newer %s result', (older, newer) =>
|
||||
createRoot(async (dispose) => {
|
||||
let resolve!: (value: Awaited<ReturnType<typeof NotificationsAPI.getHealth>>) => void;
|
||||
let reject!: (error: Error) => void;
|
||||
vi.mocked(NotificationsAPI.getHealth).mockReturnValueOnce(
|
||||
new Promise((yes, no) => {
|
||||
resolve = yes;
|
||||
reject = no;
|
||||
}),
|
||||
);
|
||||
const state = useNotificationDeliveryHealth();
|
||||
const pending = state.loadDeliveryHealth();
|
||||
if (newer === 'error') {
|
||||
vi.mocked(NotificationsAPI.getHealth).mockRejectedValueOnce(new Error('new failure'));
|
||||
} else {
|
||||
vi.mocked(NotificationsAPI.getHealth).mockResolvedValueOnce(healthWith(newer));
|
||||
}
|
||||
await state.loadDeliveryHealth();
|
||||
if (older === 'error') reject(new Error('old failure'));
|
||||
else resolve(healthWith(older));
|
||||
await pending;
|
||||
expect(state.deliveryHealth()?.queue.status).toBe(newer === 'error' ? undefined : newer);
|
||||
expect(state.deliveryHealthUnavailable()).toBe(newer === 'error');
|
||||
expect(state.deliveryNeedsAttention()).toBe(newer !== 'healthy');
|
||||
expect(state.refreshingDeliveryHealth()).toBe(false);
|
||||
dispose();
|
||||
}),
|
||||
);
|
||||
|
||||
it.each(['healthy', 'error'])(
|
||||
'keeps loading and first-load silence when older %s finishes first',
|
||||
(older) =>
|
||||
createRoot(async (dispose) => {
|
||||
let finishOld!: () => void;
|
||||
let finishNew!: () => void;
|
||||
vi.mocked(NotificationsAPI.getHealth)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve, reject) => {
|
||||
finishOld = () =>
|
||||
older === 'error'
|
||||
? reject(new Error('old failure'))
|
||||
: resolve(healthWith('healthy'));
|
||||
}),
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
finishNew = () => resolve(healthWith('degraded'));
|
||||
}),
|
||||
);
|
||||
const state = useNotificationDeliveryHealth();
|
||||
const oldRequest = state.loadDeliveryHealth();
|
||||
const newRequest = state.loadDeliveryHealth();
|
||||
finishOld();
|
||||
await oldRequest;
|
||||
expect(state.refreshingDeliveryHealth()).toBe(true);
|
||||
expect(state.deliveryHealth()).toBeNull();
|
||||
expect(state.deliveryNeedsAttention()).toBe(false);
|
||||
finishNew();
|
||||
await newRequest;
|
||||
expect(state.refreshingDeliveryHealth()).toBe(false);
|
||||
expect(state.deliveryNeedsAttention()).toBe(true);
|
||||
dispose();
|
||||
}),
|
||||
);
|
||||
|
||||
it.each(['dismissTerminalFailures', 'retryTerminalFailures'] as const)(
|
||||
'keeps post-%s health when a pre-action request finishes late',
|
||||
(action) =>
|
||||
createRoot(async (dispose) => {
|
||||
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
try {
|
||||
const state = useNotificationDeliveryHealth();
|
||||
vi.mocked(NotificationsAPI.getHealth).mockResolvedValueOnce({
|
||||
queue: { status: 'degraded', attentionRequired: 2 },
|
||||
} as never);
|
||||
await state.loadDeliveryHealth();
|
||||
let finishOld!: (health: Awaited<ReturnType<typeof NotificationsAPI.getHealth>>) => void;
|
||||
vi.mocked(NotificationsAPI.getHealth)
|
||||
.mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
finishOld = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce(healthWith('healthy'));
|
||||
const pending = state.loadDeliveryHealth();
|
||||
vi.mocked(NotificationsAPI[action]).mockResolvedValueOnce({ affected: 2 } as never);
|
||||
await state[action]();
|
||||
expect(state.deliveryNeedsAttention()).toBe(false);
|
||||
finishOld(healthWith('degraded'));
|
||||
await pending;
|
||||
expect(state.deliveryHealth()?.queue.status).toBe('healthy');
|
||||
expect(state.deliveryNeedsAttention()).toBe(false);
|
||||
} finally {
|
||||
confirmSpy.mockRestore();
|
||||
dispose();
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -22,19 +22,27 @@ export function useNotificationDeliveryHealth(options?: {
|
||||
const [refreshingDeliveryHealth, setRefreshingDeliveryHealth] = createSignal(false);
|
||||
const [loadedOnce, setLoadedOnce] = createSignal(false);
|
||||
|
||||
// Mount, configuration retry and queue actions can overlap. Only the latest
|
||||
// requested snapshot owns health and loading state, regardless of completion order.
|
||||
let latestHealthRequest = 0;
|
||||
const loadDeliveryHealth = async () => {
|
||||
const request = ++latestHealthRequest;
|
||||
setRefreshingDeliveryHealth(true);
|
||||
try {
|
||||
const health = await NotificationsAPI.getHealth();
|
||||
if (request !== latestHealthRequest) return;
|
||||
setDeliveryHealth(health);
|
||||
setDeliveryHealthUnavailable(health.queue.status === 'unavailable');
|
||||
} catch (error) {
|
||||
if (request !== latestHealthRequest) return;
|
||||
logger.error('Failed to load notification delivery health', error);
|
||||
setDeliveryHealth(null);
|
||||
setDeliveryHealthUnavailable(true);
|
||||
} finally {
|
||||
setLoadedOnce(true);
|
||||
setRefreshingDeliveryHealth(false);
|
||||
if (request === latestHealthRequest) {
|
||||
setLoadedOnce(true);
|
||||
setRefreshingDeliveryHealth(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 { render } from 'solid-js/web';
|
||||
import { Show } from 'solid-js';
|
||||
import { NotificationsAPI } from '/src/api/notifications';
|
||||
import { AlertsAPI } from '/src/api/alerts';
|
||||
import { useAlertDestinationsTabState } from '/src/features/alerts/useAlertDestinationsTabState';
|
||||
import { AlertDeliveryHealthCard } from '/src/features/alerts/AlertDeliveryHealthCard';
|
||||
import '/src/index.css';
|
||||
const pending = [];
|
||||
NotificationsAPI.getHealth = () => new Promise((resolve, reject) => pending.push({resolve, reject}));
|
||||
NotificationsAPI.getDeliveryLog = async () => [];
|
||||
AlertsAPI.getEvents = async () => [];
|
||||
NotificationsAPI.retryTerminalFailures = NotificationsAPI.dismissTerminalFailures = async () => ({affected: 1});
|
||||
window.confirm = () => true;
|
||||
window.finish = (i, status) => status === 'error' ? pending[i].reject(new Error('scripted offline')) : pending[i].resolve({queue:{status, failed:1, deadLetter:0, attentionRequired:1}});
|
||||
window.count = () => pending.length;
|
||||
function Fixture() {
|
||||
const s = useAlertDestinationsTabState({emailConfig:()=>({}), appriseConfig:()=>({}), setAppriseConfig:()=>{}, configLoadError:()=> 'scripted config unavailable', isRetrying:()=>false, isLoadingDestinations:()=>false, onRetryLoad:()=>{}, webhooks:()=>[]});
|
||||
return <main class="p-4"><h1>Delivery health ordering fixture</h1><button onClick={s.handleRetry}>Configuration Retry</button><output data-testid="loading">{String(s.refreshingDeliveryHealth())}</output><Show when={s.deliveryNeedsAttention()}><AlertDeliveryHealthCard health={s.deliveryHealth()?.queue ?? null} unavailable={s.deliveryHealthUnavailable()} refreshing={s.refreshingDeliveryHealth()} onRefresh={s.loadDeliveryHealth} onRetryFailures={s.retryTerminalFailures} onDismissFailures={s.dismissTerminalFailures}/></Show></main>;
|
||||
}
|
||||
render(() => <Fixture/>, document.getElementById('root'));
|
||||
`;
|
||||
const server = await createServer({
|
||||
root,
|
||||
configFile: false,
|
||||
optimizeDeps: {
|
||||
noDiscovery: true,
|
||||
entries: [],
|
||||
esbuildOptions: { target: "esnext" },
|
||||
},
|
||||
esbuild: { target: "esnext" },
|
||||
plugins: [
|
||||
solid(),
|
||||
{
|
||||
name: "ordering-fixture",
|
||||
configureServer(s) {
|
||||
s.middlewares.use((req, res, next) => {
|
||||
if (req.url === "/qualification") {
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.end(
|
||||
'<div id="root"></div><script type="module" src="/ordering-fixture.tsx"></script>',
|
||||
);
|
||||
} else next();
|
||||
});
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id === "/ordering-fixture.tsx") return id;
|
||||
},
|
||||
load(id) {
|
||||
if (id === "/ordering-fixture.tsx") return fixture;
|
||||
},
|
||||
},
|
||||
],
|
||||
resolve: { alias: { "@": resolve(root, "src") } },
|
||||
server: { host: "127.0.0.1", port: 5197, strictPort: true },
|
||||
});
|
||||
let browser;
|
||||
try {
|
||||
await server.listen();
|
||||
browser = await chromium.launch({ headless: true });
|
||||
mkdirSync("/tmp/pulse-health-ordering", { recursive: true });
|
||||
let cases = 0;
|
||||
for (const width of [1440, 390]) {
|
||||
for (const [old, newer] of [
|
||||
["healthy", "degraded"],
|
||||
["degraded", "healthy"],
|
||||
["error", "healthy"],
|
||||
["healthy", "error"],
|
||||
]) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
||||
await page.goto("http://127.0.0.1:5197/qualification");
|
||||
await page.waitForFunction(() => window.count?.() === 1);
|
||||
await page
|
||||
.getByRole("button", { name: "Configuration Retry", exact: true })
|
||||
.click();
|
||||
await page.waitForFunction(() => window.count() === 2);
|
||||
await page.evaluate((s) => window.finish(1, s), newer);
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("output").textContent === "false",
|
||||
);
|
||||
const before = await page.locator("main").innerText();
|
||||
await page.evaluate((s) => window.finish(0, s), old);
|
||||
await page.evaluate(
|
||||
() =>
|
||||
new Promise((r) =>
|
||||
requestAnimationFrame(() => requestAnimationFrame(r)),
|
||||
),
|
||||
);
|
||||
assert.equal(await page.locator("main").innerText(), before);
|
||||
assert.equal(
|
||||
await page.getByRole("alert").count(),
|
||||
["degraded", "error"].includes(newer) ? 1 : 0,
|
||||
);
|
||||
await page.screenshot({
|
||||
path: `/tmp/pulse-health-ordering/${width}-${old}-${newer}.png`,
|
||||
});
|
||||
await page.close();
|
||||
cases++;
|
||||
}
|
||||
for (const action of [
|
||||
"Retry retained deliveries",
|
||||
"Dismiss retained failures",
|
||||
]) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 } });
|
||||
await page.goto("http://127.0.0.1:5197/qualification");
|
||||
await page.waitForFunction(() => window.count?.() === 1);
|
||||
await page.evaluate(() => window.finish(0, "degraded"));
|
||||
await page.getByRole("alert").waitFor();
|
||||
await page
|
||||
.getByRole("button", { name: "Configuration Retry", exact: true })
|
||||
.click();
|
||||
await page.getByRole("button", { name: action, exact: true }).click();
|
||||
await page.waitForFunction(() => window.count() === 3);
|
||||
await page.evaluate(() => window.finish(1, "healthy"));
|
||||
assert.equal(await page.getByTestId("loading").textContent(), "true");
|
||||
await page.evaluate(() => window.finish(2, "healthy"));
|
||||
await page.waitForFunction(
|
||||
() => document.querySelector("output").textContent === "false",
|
||||
);
|
||||
assert.equal(await page.getByRole("alert").count(), 0);
|
||||
await page.close();
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
result: "passed",
|
||||
cases,
|
||||
viewports: [1440, 390],
|
||||
scope:
|
||||
"real Chromium, real caller/hook/card, scripted API; not installed application or notification receipt",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
await browser?.close();
|
||||
await server.close();
|
||||
}
|
||||
Reference in New Issue
Block a user