Merge candidate 20260908T021505Z-web-product

Change-source: pulse-maintainer

# Conflicts:
#	docs/release-control/v6/internal/subsystems/alerts.md
#	docs/release-control/v6/internal/subsystems/frontend-primitives.md
#	frontend-modern/browser-verification.json
#	scripts/check-incident-request-ownership.mjs
This commit is contained in:
pulse-triage[bot]
2026-09-08 03:36:54 +01:00
5 changed files with 78 additions and 8 deletions
@@ -33,6 +33,20 @@ Unknown timestamps remain unavailable. Truncation is explicit. The Assistant
handoff preserves occurrence identity and the latest bounded event window, and
labels history as insufficient to establish current health.
### Incident resource error-state ownership
Resource incident errors follow the same per-resource latest-request ownership
as history and loading. Superseded or disposed reads cannot set an error;
reset clears errors and invalidates pending reads. A retry clears the current
error while retaining cached history until the current request succeeds.
A stale success cannot clear a newer failure. This reconciles the merged error
accessor with lifecycle protection, without changing notification delivery.
Verification: resource hook lifecycle assertions cover ordering and retry;
`scripts/check-incident-request-ownership.mjs` checks the real hook and panel
in Chromium at desktop and narrow widths, including error state and retry.
Its scripted fixture is not full merged-UI or installed delivery acceptance.
### Retained-queue recovery feedback has no reading deadline
Retry and Dismiss failures have a view-local untimed equivalent beside the
@@ -7254,7 +7254,9 @@ disposal invalidates them and prevents new loads. Overlapping reads for differen
resources remain independent. Closing a row still permits its in-flight result
to populate the existing cache; reopening cached history and explicit refresh
are unchanged. Requests are not transport-cancelled. No API, retention or
notification-delivery policy changes.
notification-delivery policy changes. A retry clears the current failed-read
state while retaining cached history until the owning request succeeds, and a
superseded success cannot clear a newer failure.
The hook's ten ordinary regression/control cases cover success, catch and
finally writes, reset/reopen and disposal. The existing panel tests cover its
+9 -3
View File
@@ -75,7 +75,9 @@
"Saved operator note and unsaved draft across desktop/phone layout changes, long-note wrapping and persisted reload.",
"Timeline/resource handoff into Assistant, reopened composer focus, saved funded history-and-note response across widths.",
"Request reset, reverse completion, superseded success/failure, current failure, and success/failure after disposal.",
"Latest per-resource owner controls incident content, failed-read state, notifications, and loading completion."
"Latest per-resource owner controls incident content, failed-read state, notifications, and loading completion.",
"Retry clears the current failed-read state while retaining cached history until the owning response succeeds.",
"Obsolete and disposed failures do not set the resource error accessor."
],
"interactions": [
"Expand/collapse native Evidence details by keyboard, All/None event filters and note draft enable/clear.",
@@ -85,6 +87,10 @@
"Resume the exact saved upstream session, scroll through its note explanation and inspect actual pixels.",
"Open a resource row, overlap refresh, reset or unmount, and resolve or reject deferred reads in controlled order."
],
"notes": "The protected upstream receipt remains exact for 13 unchanged runtime files. The sole combined-content change, useAlertResourceIncidentsState.ts, was freshly exercised with its real panel in 14 Chromium cases at desktop and phone widths after adding ownership to the upstream failed-read signal. Screenshots are retained under /tmp/pulse-incident-ownership. This is component lifecycle evidence, not an installed Alerts route, provider delivery, touch-device, production rollout, backup/restore, or population-reliability receipt.",
"command": "pulse-heavy-run -- node scripts/check-incident-request-ownership.mjs"
"notes": "The protected upstream receipt remains exact for 13 unchanged runtime files. The sole combined-content change, useAlertResourceIncidentsState.ts, was freshly exercised with its real panel in 16 Chromium cases at desktop and phone widths after adding ownership to the upstream failed-read signal. Screenshots are retained under /tmp/pulse-incident-ownership. This is component lifecycle evidence, not an installed Alerts route, provider delivery, touch-device, production rollout, backup/restore, or population-reliability receipt.",
"command": "pulse-heavy-run -- node scripts/check-incident-request-ownership.mjs",
"limitations": [
"The 16-case qualification route uses fixture lifecycle controls and a scripted API; the supplied panel does not expose the error accessor directly, so that state is asserted through the fixture snapshot.",
"This is not installed acceptance, provider delivery, touch-device, production rollout, backup/restore, or population-reliability proof."
]
}
@@ -82,8 +82,8 @@ describe('resource incident request ownership', () => {
cleanup();
pending.reject(new Error('disposed read'));
await load;
expect(notificationStore.error).not.toHaveBeenCalled();
expect(result.resourceIncidentError().host).toBe(false);
expect(notificationStore.error).not.toHaveBeenCalled();
});
it('keeps a reopened request owned after an older reset-era failure', async () => {
@@ -99,6 +99,7 @@ describe('resource incident request ownership', () => {
old.reject(new Error('reset-era failure'));
await load;
expect(result.resourceIncidentLoading().host).toBe(true);
expect(result.resourceIncidentError().host).toBe(false);
expect(notificationStore.error).not.toHaveBeenCalled();
current.resolve([]);
await reopened;
@@ -116,10 +117,10 @@ describe('resource incident request ownership', () => {
await result.refreshResourceIncidentPanel();
old.reject(new Error('obsolete failure'));
await load;
expect(result.resourceIncidentError().host).toBe(false);
expect(notificationStore.error).not.toHaveBeenCalled();
expect(result.resourceIncidents().host).toEqual([]);
expect(result.resourceIncidentLoading().host).toBe(false);
expect(result.resourceIncidentError().host).toBe(false);
});
it('ignores successful reads and new loads after disposal', async () => {
@@ -152,7 +153,46 @@ describe('resource incident request ownership', () => {
await load;
expect(result.resourceIncidents().first).toEqual([]);
expect(result.resourceIncidentLoading()).toEqual({ first: false, second: false });
expect(result.resourceIncidentError()).toEqual({ first: false, second: true });
});
it('preserves the current error when a superseded success arrives', async () => {
const old = deferred();
vi.mocked(AlertsAPI.getIncidentsForResource)
.mockReturnValueOnce(old.promise)
.mockRejectedValueOnce(new Error('current failure'));
const { result } = renderHook(useAlertResourceIncidentsState);
const load = result.openResourceIncidentPanel('host', 'Host', 'row');
await result.refreshResourceIncidentPanel();
old.resolve([]);
await load;
expect(result.resourceIncidentError().host).toBe(true);
expect(result.resourceIncidents()).toEqual({});
expect(notificationStore.error).toHaveBeenCalledTimes(1);
});
it('clears a current error on retry without discarding cached history', async () => {
const cached = [{ id: 'retained' }] as Incidents;
const retry = deferred();
vi.mocked(AlertsAPI.getIncidentsForResource)
.mockResolvedValueOnce(cached)
.mockRejectedValueOnce(new Error('refresh failed'))
.mockReturnValueOnce(retry.promise);
const { result } = renderHook(useAlertResourceIncidentsState);
await result.openResourceIncidentPanel('host', 'Host', 'row');
await result.refreshResourceIncidentPanel();
expect(result.resourceIncidentError().host).toBe(true);
expect(result.resourceIncidents().host).toEqual(cached);
const refresh = result.refreshResourceIncidentPanel();
expect(result.resourceIncidentError().host).toBe(false);
expect(result.resourceIncidentLoading().host).toBe(true);
retry.resolve([]);
await refresh;
expect(result.resourceIncidentError().host).toBe(false);
expect(result.resourceIncidents().host).toEqual([]);
result.resetResourceIncidentsState();
expect(result.resourceIncidentError()).toEqual({});
});
it('toggles the opening row and reuses loaded history for another row', async () => {
vi.mocked(AlertsAPI.getIncidentsForResource).mockResolvedValue([]);
const { result } = renderHook(useAlertResourceIncidentsState);
+10 -2
View File
@@ -83,6 +83,7 @@ try {
"dispose-success",
"dispose-failure",
"current-failure",
"retry",
]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
await page.goto("http://127.0.0.1:5198/qualification");
@@ -99,8 +100,15 @@ try {
(s) => window.finish(0, s === "dispose-failure" ? "error" : "empty"),
scenario,
);
} else if (scenario === "current-failure") {
} else if (scenario === "current-failure" || scenario === "retry") {
await page.evaluate(() => window.finish(0, "error"));
await page.waitForFunction(() => window.snapshot().error.host === true);
if (scenario === "retry") {
await page.getByRole("button", { name: "Overlap refresh", exact: true }).click();
await page.waitForFunction(() => window.count() === 2);
assert.equal((await page.evaluate(() => window.snapshot())).error.host, false);
await page.evaluate(() => window.finish(1, "Latest incident"));
}
} else {
await page
.getByRole("button", { name: "Overlap refresh", exact: true })
@@ -143,7 +151,7 @@ try {
const snapshot = await page.evaluate(() => window.snapshot());
assert.equal(
await page.getByTestId("errors").textContent(),
scenario === "current-failure" ? "1" : "0",
["current-failure", "retry"].includes(scenario) ? "1" : "0",
);
if (scenario === "reset") {
assert.deepEqual(snapshot, { incidents: {}, loading: {}, error: {} });