diff --git a/src/main/browser/extensions/browser-extension-action-registry.ts b/src/main/browser/extensions/browser-extension-action-registry.ts index c832a76aee3..0520c3da580 100644 --- a/src/main/browser/extensions/browser-extension-action-registry.ts +++ b/src/main/browser/extensions/browser-extension-action-registry.ts @@ -82,6 +82,10 @@ export function getBrowserExtensionActionState( return state } +export function forgetBrowserExtensionActionProfile(profileId: string): void { + stateByProfileId.delete(profileId) +} + /** A tab's action overrides end when it loads a new document or closes (Chrome semantics). */ export function forgetBrowserExtensionActionTab(profileId: string, tabId: number): void { let changed = false diff --git a/src/main/browser/extensions/browser-extension-session-loader.test.ts b/src/main/browser/extensions/browser-extension-session-loader.test.ts index 2b8d8de9054..494cd7f660e 100644 --- a/src/main/browser/extensions/browser-extension-session-loader.test.ts +++ b/src/main/browser/extensions/browser-extension-session-loader.test.ts @@ -81,6 +81,7 @@ vi.mock('node:fs', async (importOriginal) => ({ vi.mock('./browser-extension-api-host', () => ({ attachBrowserExtensionApiHostToSession: vi.fn(), forgetBrowserExtensionContexts: vi.fn(), + getBrowserExtensionProfileIdForSession: vi.fn(() => null), emitBrowserExtensionWorkerStartEvent: mocks.emitWorkerStart })) vi.mock('./browser-extension-load-directory', () => ({ @@ -106,6 +107,7 @@ const { getBrowserExtensionProfileStateStore } = const { getBrowserExtensionSyncStorageStore } = await import('./browser-extension-sync-storage-store') const { getBrowserExtensionInstallStore } = await import('./browser-extension-install-store') +const { getBrowserExtensionActionState } = await import('./browser-extension-action-registry') const realFs = await vi.importActual('node:fs') const installed = { @@ -372,6 +374,67 @@ describe('profile changes while startup loads are in flight', () => { }) }) +describe('deleted profile action state', () => { + it.each([ + ['unattached', false, false], + ['attached', true, false], + ['failed-unload', true, true] + ] as const)('retires action payloads for %s', async (label, attached, failedUnload) => { + const profileId = `action-retirement-${label}` + const profile = { + id: profileId, + scope: 'isolated' as const, + partition: `persist:orca-${profileId}`, + label: 'Retired', + source: null + } + const extension = { + id: ID, + name: 'Vault', + path: installed.path, + url: `chrome-extension://${ID}/`, + version: installed.version, + manifest: { action: { default_title: 'Fresh' } } + } + setBrowserExtensionSupportEnabled(true) + if (attached) { + loader.attachBrowserExtensionsToProfileSession(profile, []) + await loader.whenBrowserExtensionProfileLoaded(profileId) + } + const removed = getBrowserExtensionActionState(profileId, extension) + removed.set('title', 'Retired title') + removed.set('icon', { kind: 'data', dataUrl: 'data:image/png;base64,retired' }) + removed.set('badgeText', 'Retired tab badge', 77) + const survivor = getBrowserExtensionActionState(`survivor-${profileId}`, extension) + survivor.set('title', 'Surviving title') + + if (failedUnload) { + mocks.loaded.set(ID, { id: ID }) + const unload = vi + .spyOn(mocks.session.extensions, 'removeExtension') + .mockImplementationOnce(() => { + throw new Error('native unload failed') + }) + try { + await expect(loader.detachBrowserExtensionsFromProfileSession(profile)).rejects.toThrow( + 'native unload failed' + ) + } finally { + unload.mockRestore() + mocks.loaded.delete(ID) + } + } else { + await loader.detachBrowserExtensionsFromProfileSession(profile) + } + + const fresh = getBrowserExtensionActionState(profileId, extension) + expect(fresh).not.toBe(removed) + expect(fresh.get(77)).toMatchObject({ title: 'Fresh', icon: null, badgeText: '' }) + expect(getBrowserExtensionActionState(`survivor-${profileId}`, extension)).toBe(survivor) + expect(survivor.get().title).toBe('Surviving title') + }) +}) + it('does not restore stale enablement after uninstall and reinstall in another Orca profile', async () => { setBrowserExtensionSupportEnabled(true) await loader.detachBrowserExtensionsFromAllProfileSessions() diff --git a/src/main/browser/extensions/browser-extension-session-loader.ts b/src/main/browser/extensions/browser-extension-session-loader.ts index 143c59fcc02..e1c6d9b00a8 100644 --- a/src/main/browser/extensions/browser-extension-session-loader.ts +++ b/src/main/browser/extensions/browser-extension-session-loader.ts @@ -1,4 +1,5 @@ import { session } from 'electron' +import { forgetBrowserExtensionActionProfile } from './browser-extension-action-registry' import { existsSync } from 'node:fs' import { join } from 'node:path' import type { BrowserSessionProfile } from '../../../shared/browser-workspace-types' @@ -168,8 +169,12 @@ export async function detachBrowserExtensionsFromProfileSession( await retireBrowserExtensionProfileStorage(sess, profile.partition).catch((error: unknown) => { console.warn('[browser-extensions] Could not clear deleted profile storage:', error) }) - if (state) { - await unloadProfileSession(state) + try { + if (state) { + await unloadProfileSession(state) + } + } finally { + forgetBrowserExtensionActionProfile(profile.id) } getBrowserExtensionProfileStateStore().forgetProfile(profile.id) getBrowserExtensionSyncStorageStore().forgetProfile(profile.id)