fix(browser): retire deleted profile extension action state (#34988)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 05:36:26 -07:00
committed by GitHub
co-authored by OrcaWin
parent 760e311693
commit f50425f2ec
3 changed files with 74 additions and 2 deletions
@@ -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
@@ -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<typeof NodeFs>('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()
@@ -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)