From e7e8bdd2f98280e47fb34b432be01e4be14302b3 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 28 Oct 2026 10:19:22 -0700 Subject: [PATCH] Retire failed extension notification records (#35134) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- .../extensions/api/notifications.test.ts | 40 +++++++++++++++++++ .../browser/extensions/api/notifications.ts | 11 ++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/main/browser/extensions/api/notifications.test.ts b/src/main/browser/extensions/api/notifications.test.ts index 8be7eceda2e..b099b9db9ca 100644 --- a/src/main/browser/extensions/api/notifications.test.ts +++ b/src/main/browser/extensions/api/notifications.test.ts @@ -82,6 +82,46 @@ function invoke(caller: BrowserExtensionCaller, method: string, args: unknown[] const options = { type: 'basic', title: 'Notification', message: 'Retire with its extension' } describe('notification extension ownership', () => { + it.each(['create', 'update'])( + 'forgets a failed native %s without reporting a dismissal', + (method) => { + const { caller } = notificationCaller(`failed-${method}`) + const sibling = { ...caller, extension: { ...caller.extension, id: 'b'.repeat(32) } } + invoke(caller, 'create', ['failed', options]) + if (method === 'update') { + invoke(caller, 'update', ['failed', { message: 'updated' }]) + } + const failed = mocks.notifications.at(-1) + if (!failed) { + throw new Error('Missing native notification') + } + invoke(sibling, 'create', ['live', options]) + vi.clearAllMocks() + + failed.emit('failed', {}, 'Native notification delivery failed') + + expect(invoke(caller, 'getAll')).toEqual({}) + expect(invoke(caller, 'update', ['failed', { message: 'retry' }])).toBe(false) + expect(invoke(caller, 'clear', ['failed'])).toBe(false) + expect(invoke(sibling, 'getAll')).toEqual({ live: true }) + expect(failed.eventNames()).toEqual([]) + expect(failed.close).not.toHaveBeenCalled() + expect(emitBrowserExtensionEvent).not.toHaveBeenCalled() + + invoke(caller, 'create', ['failed', options]) + failed.emit('failed', {}, 'Late old failure') + failed.emit('close', { reason: 'timedOut' }) + expect(invoke(caller, 'getAll')).toEqual({ failed: true }) + expect(invoke(caller, 'clear', ['failed'])).toBe(true) + expect(emitBrowserExtensionEvent).toHaveBeenLastCalledWith( + { session: caller.session, extensionId: caller.extension.id }, + 'notifications', + 'onClosed', + ['failed', false] + ) + } + ) + it('closes and forgets unloaded notifications while preserving sibling extensions and profiles', () => { const { caller, extensions } = notificationCaller('owner') const sibling = { ...caller, extension: { ...caller.extension, id: 'b'.repeat(32) } } diff --git a/src/main/browser/extensions/api/notifications.ts b/src/main/browser/extensions/api/notifications.ts index f0dc486975d..000f1a10a99 100644 --- a/src/main/browser/extensions/api/notifications.ts +++ b/src/main/browser/extensions/api/notifications.ts @@ -93,12 +93,14 @@ function emit(caller: BrowserExtensionCaller, event: string, args: unknown[]): v ) } -function detach(entry: Shown): void { +function detach(entry: Shown, closeNative = true): void { const { notification } = entry entry.notification = null if (notification) { notification.removeAllListeners() - notification.close() + if (closeNative) { + notification.close() + } } } @@ -138,6 +140,11 @@ function show(caller: BrowserExtensionCaller, id: string, entry: Shown): void { shownFor(caller).delete(id) emit(caller, 'onClosed', [id, details.reason !== 'timedOut']) }) + notification.once('failed', () => { + shownFor(caller).delete(id) + // Electron destroys the native notification after dispatching this failure. + detach(entry, false) + }) entry.notification = notification notification.show() }