diff --git a/src/main/startup/main-process-error-guards.test.ts b/src/main/startup/main-process-error-guards.test.ts index a62a0cd8a00..a9fae48ecb4 100644 --- a/src/main/startup/main-process-error-guards.test.ts +++ b/src/main/startup/main-process-error-guards.test.ts @@ -153,6 +153,33 @@ describe('main-process fatal error guards (issue #9441)', () => { ) }) + it('reopens the window when the wall clock jumps backwards after exhaustion', async () => { + vi.resetModules() + const record = vi.fn() + vi.doMock('../crash-reporting/durable-crash-breadcrumb', () => ({ + recordDurableCrashBreadcrumb: record + })) + const { recordFatalMainProcessError } = await import('./main-process-error-guards') + vi.spyOn(console, 'error').mockImplementation(() => {}) + let now = 1_000_000 + vi.spyOn(Date, 'now').mockImplementation(() => now) + + for (let i = 0; i < 25; i++) { + recordFatalMainProcessError('main_unhandled_rejection', new Error(`storm ${i}`)) + } + expect(record).toHaveBeenCalledTimes(20) + + // Why: a backward jump must not trap the exhausted window and suppress every later breadcrumb. + now -= 3_600_000 + recordFatalMainProcessError('main_unhandled_rejection', new Error('after backward jump')) + expect(record).toHaveBeenCalledTimes(21) + expect(record).toHaveBeenLastCalledWith( + 'main_unhandled_rejection', + expect.objectContaining({ errorMessage: 'after backward jump', suppressedSinceLast: 5 }), + 'main_unhandled_rejection' + ) + }) + it('never suppresses the fatal uncaught-exception record after a rejection storm', async () => { vi.resetModules() const record = vi.fn() diff --git a/src/main/startup/main-process-error-guards.ts b/src/main/startup/main-process-error-guards.ts index 22368c23e73..fcf178c67be 100644 --- a/src/main/startup/main-process-error-guards.ts +++ b/src/main/startup/main-process-error-guards.ts @@ -66,7 +66,8 @@ export function recordFatalMainProcessError(kind: FatalMainProcessErrorKind, err // must never be lost to a window a storm already exhausted. if (kind === 'main_unhandled_rejection') { const now = Date.now() - if (now - recordWindowStartedAt >= RECORD_WINDOW_MS) { + // Why: a backward clock jump (sleep/resume, NTP) would otherwise trap an exhausted window and suppress every breadcrumb until wall time catches up. + if (now < recordWindowStartedAt || now - recordWindowStartedAt >= RECORD_WINDOW_MS) { recordWindowStartedAt = now recordWindowCount = 0 }