mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
|
||||
+42
-2
@@ -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);
|
||||
|
||||
@@ -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: {} });
|
||||
|
||||
Reference in New Issue
Block a user