From d85576e50bc57527674b7d69e606755b32397384 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 1 Aug 2026 05:09:25 -0700 Subject: [PATCH] fix(crash-reporting): keep error-boundary reports out of the sibling sweep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relation matching ignores processType, so a react-render report was swept the moment it shared reason and exitCode with the anchor. Unreachable today only because the boundary reason literal never collides with an Electron exit reason — an invariant held 200 lines away with nothing pinning it. Restates the anchor pass's own exclusion rather than reusing isReactErrorBoundaryReport(), which tests that same reason literal. Both regression tests now use a crash-shaped reason/exitCode so they fail without the guard instead of passing on the mismatch. Co-authored-by: Orca --- .../crash-reporting/crash-report-store.test.ts | 15 +++++++++++++++ src/main/crash-reporting/crash-report-store.ts | 8 +++++++- ...sh-reporting-recovered-renderer-prompt.test.ts | 9 ++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/crash-reporting/crash-report-store.test.ts b/src/main/crash-reporting/crash-report-store.test.ts index 1a14a9fa2e6..ed08a903f79 100644 --- a/src/main/crash-reporting/crash-report-store.test.ts +++ b/src/main/crash-reporting/crash-report-store.test.ts @@ -110,6 +110,21 @@ describe('CrashReportStore', () => { await expect(store.getLatestPending()).resolves.toBeNull() }) + it('never sweeps a React error-boundary report, even when it matches the crash event', async () => { + const { store } = await createStore() + // Same reason/exitCode as the process crash: the guard must not depend on + // the boundary reason literal staying distinct from Electron's exit reasons. + const boundary = await store.record({ ...input(), processType: 'react-render' }) + await store.record(input()) + + await store.markRendererCrashesAutoRecovered(Date.parse(boundary.createdAt) - 1_000) + + await expect(store.getLatestPending()).resolves.toMatchObject({ + id: boundary.id, + status: 'pending' + }) + }) + it('dismisses sibling pending records after one crash report is sent', async () => { const { store } = await createStore() await store.record(input()) diff --git a/src/main/crash-reporting/crash-report-store.ts b/src/main/crash-reporting/crash-report-store.ts index 99603af89ce..6ca3ff63f17 100644 --- a/src/main/crash-reporting/crash-report-store.ts +++ b/src/main/crash-reporting/crash-report-store.ts @@ -166,7 +166,13 @@ export class CrashReportStore { // healed crash still reaches the user, just wearing the sibling's report. const anchors = [...resolved] const sweptReports = nextReports.map((report) => { - if (report.status !== 'pending') { + // Why: relation matching ignores processType, so re-state the anchor + // pass's exclusion — a renderer-sourced report that is not the renderer + // process is the error boundary reporting itself, not a burst sibling. + if ( + report.status !== 'pending' || + (report.source === 'renderer' && report.processType !== 'renderer') + ) { return report } if (!anchors.some((anchor) => isRelatedCrashEvent(anchor, report))) { diff --git a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts index 04b4540894d..b3454423ecc 100644 --- a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts +++ b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts @@ -199,7 +199,14 @@ describe('auto-recovered renderer crash reporting', () => { it('resolves the process crash without sweeping a React error-boundary crash beside it', async () => { const store = await createStore() registerCrashReportingHandlers(store) - const boundary = await store.record(reactErrorBoundaryCrash()) + // Why the crash-shaped reason/exitCode: relation matching ignores + // processType, so a boundary report only survives the sweep on its own + // guard — not on the reason literal happening not to collide. + const boundary = await store.record({ + ...reactErrorBoundaryCrash(), + reason: 'killed', + exitCode: 9 + }) await store.record(killedRendererCrash()) noteRendererRecoveryReloadIssued()