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',