From 7fbf3130ace1a4c0f68d733809c754b48e08a5ef Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:39:52 -0700 Subject: [PATCH] fix(editor): stop filing crash reports for expected lazy-chunk swaps RichMarkdownErrorBoundary reported every caught error as a react-error-boundary crash, including the LazyChunkLoadError sentinel that lazy-with-retry throws after it has already exhausted its retries and its one guarded reload. That sentinel means "the chunk hash changed under a running window" (an app update), which is deliberate graceful degradation, not a crash. RecoverableRenderErrorBoundary already skips reporting it (#6206); this boundary was never updated. Crash b860def2 is exactly that path: a lazy_chunk_reload breadcrumb ("Unexpected token ':'") fires first, then the post-reload attempt surfaces LazyChunkLoadError and files a report. The fallback UI is unchanged, so the pane stays usable and offers retry. --- ...hMarkdownErrorBoundary.lazy-chunk.test.tsx | 112 ++++++++++++++++++ .../editor/RichMarkdownErrorBoundary.tsx | 7 ++ 2 files changed, 119 insertions(+) create mode 100644 src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx diff --git a/src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx b/src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx new file mode 100644 index 00000000000..90046bf0d9c --- /dev/null +++ b/src/renderer/src/components/editor/RichMarkdownErrorBoundary.lazy-chunk.test.tsx @@ -0,0 +1,112 @@ +// @vitest-environment happy-dom + +import { Suspense, act, type ReactElement, type ReactNode } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { lazyWithRetry } from '@/lib/lazy-with-retry' +import { RichMarkdownErrorBoundary } from './RichMarkdownErrorBoundary' + +const reportCrashMock = vi.hoisted(() => vi.fn()) + +vi.mock('@/lib/react-error-boundary-reporting', () => ({ + reportReactErrorBoundaryCrash: reportCrashMock +})) + +const RELOAD_GUARD_KEY = 'orca:lazy-chunk-reload-attempted' + +globalThis.IS_REACT_ACT_ENVIRONMENT = true + +function createContainer(): { container: HTMLDivElement; root: Root } { + const container = document.createElement('div') + document.body.appendChild(container) + return { container, root: createRoot(container) } +} + +// Mirrors EditorContent: the Suspense sits above the boundary, which wraps the lazy editor. +function BoundaryHarness({ children }: { children: ReactNode }): ReactElement { + return ( + Loading...}> + {children} + + ) +} + +async function flushReactWork(): Promise { + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) +} + +describe('RichMarkdownErrorBoundary lazy chunk containment', () => { + let root: Root | null = null + let container: HTMLDivElement | null = null + let consoleError: ReturnType + + beforeEach(() => { + reportCrashMock.mockReset() + window.sessionStorage.clear() + consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + }) + + afterEach(() => { + if (root) { + act(() => root?.unmount()) + } + container?.remove() + root = null + container = null + window.sessionStorage.clear() + consoleError.mockRestore() + }) + + // Repro for crash b860def2: an app update swapped chunk hashes, lazy-with-retry + // burned its one guarded reload, then surfaced LazyChunkLoadError here. + it('renders the fallback without reporting after guarded dynamic import exhaustion', async () => { + window.sessionStorage.setItem(RELOAD_GUARD_KEY, '1') + const LazyRejectingImport = lazyWithRetry( + () => Promise.reject(new SyntaxError("Unexpected token ':'")), + { retries: 0 } + ) + ;({ container, root } = createContainer()) + + await act(async () => { + root?.render( + + + + ) + }) + await flushReactWork() + await flushReactWork() + + expect(container?.textContent).toContain('rich markdown editor') + expect(reportCrashMock).not.toHaveBeenCalled() + }) + + it('still reports ordinary render errors', async () => { + const error = new Error('ordinary render failure') + function BrokenEditor(): ReactElement { + throw error + } + ;({ container, root } = createContainer()) + + await act(async () => { + root?.render( + + + + ) + }) + + expect(container?.textContent).toContain('rich markdown editor') + expect(reportCrashMock).toHaveBeenCalledTimes(1) + expect(reportCrashMock).toHaveBeenCalledWith( + expect.objectContaining({ + boundaryId: 'editor.rich-markdown', + surface: 'rich-markdown-editor', + error + }) + ) + }) +}) diff --git a/src/renderer/src/components/editor/RichMarkdownErrorBoundary.tsx b/src/renderer/src/components/editor/RichMarkdownErrorBoundary.tsx index 105be54524f..68ae1ed819b 100644 --- a/src/renderer/src/components/editor/RichMarkdownErrorBoundary.tsx +++ b/src/renderer/src/components/editor/RichMarkdownErrorBoundary.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { isLazyChunkLoadError } from '@/lib/lazy-with-retry' import { reportReactErrorBoundaryCrash } from '@/lib/react-error-boundary-reporting' import { translate } from '@/i18n/i18n' @@ -38,6 +39,12 @@ export class RichMarkdownErrorBoundary extends React.Component { componentDidCatch(error: Error, info: React.ErrorInfo): void { console.error('[RichMarkdownEditor] render crash contained by boundary', error, info) + // Why: lazy-with-retry already spent its retries and its one guarded reload, so a + // chunk-hash swap after an app update is expected degradation, not a crash. The + // fallback below still renders (matches RecoverableRenderErrorBoundary). + if (isLazyChunkLoadError(error)) { + return + } void reportReactErrorBoundaryCrash({ boundaryId: 'editor.rich-markdown', surface: 'rich-markdown-editor',