Files
orca/src/main/browser/browser-session-registry.test.ts
T
42de074152 fix(browser): keep isolated profile storage on its partition (#6958)
* fix(browser): keep isolated profile storage on its partition

* fix(browser): keep cloned isolated tabs on their resolved partition

* fix(browser): thread resolved partition through UI profile switches

Keep the isolated-storage invariant universal: UI-initiated profile
switch/create now persist the resolved partition alongside the profile
id, matching the runtime path, so a tab stays on its partition even if
the renderer profile mirror is later stale (issue #6923).

Co-authored-by: Orca <help@stably.ai>

* fix(browser): persist resolved partition through session restore

Add sessionPartition to the workspace-session zod schema so the resolved
partition survives persist->load; without it zod strips the field and a
restored isolated tab whose profile mirror is stale at startup falls back
to the shared default partition, reopening the storage leak (#6923).

Fold the webview teardown branches (parent drift or partition mismatch)
into one and re-resolve the viewport container once, matching the
pre-refactor null-guard behavior.

Split the sleeping-agent parse tests into their own file to keep both
under the 800-line cap.

Co-authored-by: Orca <help@stably.ai>

* fix(browser): keep CLI-created tabs inheriting the default profile

browserTabCreate without an explicit profile was sending sessionProfileId:
null, which the renderer store treats as 'no inheritance' (its guard is
!== undefined). That forced CLI-created tabs onto the shared default
partition even when the user had configured a default browser session
profile, silently changing behavior a #6923 fix should not touch.

Leave sessionProfileId/sessionPartition undefined when no profile is named
so the renderer applies default-profile inheritance; thread the resolved
partition only when a profile is explicitly chosen.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-03 13:58:40 -07:00

351 lines
14 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const { sessionFromPartitionMock, askForMediaAccessMock, getMediaAccessStatusMock } = vi.hoisted(
() => ({
sessionFromPartitionMock: vi.fn(),
askForMediaAccessMock: vi.fn(),
getMediaAccessStatusMock: vi.fn()
})
)
vi.mock('electron', () => ({
session: {
fromPartition: sessionFromPartitionMock
},
systemPreferences: {
askForMediaAccess: askForMediaAccessMock,
getMediaAccessStatus: getMediaAccessStatusMock
}
}))
vi.mock('./browser-manager', () => ({
browserManager: {
notifyPermissionDenied: vi.fn(),
handleGuestWillDownload: vi.fn()
}
}))
import { browserSessionRegistry } from './browser-session-registry'
import { setupClientHintsOverride } from './browser-session-ua'
import { ORCA_BROWSER_PARTITION } from '../../shared/constants'
describe('BrowserSessionRegistry', () => {
beforeEach(() => {
sessionFromPartitionMock.mockReset()
askForMediaAccessMock.mockReset()
getMediaAccessStatusMock.mockReset()
askForMediaAccessMock.mockResolvedValue(true)
getMediaAccessStatusMock.mockReturnValue('granted')
sessionFromPartitionMock.mockReturnValue({
setPermissionRequestHandler: vi.fn(),
setPermissionCheckHandler: vi.fn(),
setDevicePermissionHandler: vi.fn(),
setDisplayMediaRequestHandler: vi.fn(),
on: vi.fn(),
removeListener: vi.fn(),
clearStorageData: vi.fn().mockResolvedValue(undefined),
clearCache: vi.fn().mockResolvedValue(undefined)
})
})
it('has a default profile on construction', () => {
const defaultProfile = browserSessionRegistry.getDefaultProfile()
expect(defaultProfile.id).toBe('default')
expect(defaultProfile.scope).toBe('default')
expect(defaultProfile.partition).toBe(ORCA_BROWSER_PARTITION)
})
it('allows the default partition', () => {
expect(browserSessionRegistry.isAllowedPartition(ORCA_BROWSER_PARTITION)).toBe(true)
})
it('rejects unknown partitions', () => {
expect(browserSessionRegistry.isAllowedPartition('persist:evil-partition')).toBe(false)
})
it('creates an isolated profile with a unique partition', () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Test Isolated')
expect(profile).not.toBeNull()
expect(profile!.scope).toBe('isolated')
expect(profile!.partition).toMatch(/^persist:orca-browser-session-/)
expect(profile!.partition).not.toBe(ORCA_BROWSER_PARTITION)
expect(profile!.label).toBe('Test Isolated')
expect(profile!.source).toBeNull()
})
it('rejects creating a profile with scope default', () => {
const profile = browserSessionRegistry.createProfile('default', 'Sneaky')
expect(profile).toBeNull()
})
it('allows created profile partitions', () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Allowed')
expect(profile).not.toBeNull()
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(true)
})
it('creates an imported profile', () => {
const profile = browserSessionRegistry.createProfile('imported', 'My Import')
expect(profile).not.toBeNull()
expect(profile!.scope).toBe('imported')
expect(profile!.partition).toMatch(/^persist:orca-browser-session-/)
})
it('resolves partition for a known profile', () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Resolve Test')
expect(profile).not.toBeNull()
expect(browserSessionRegistry.resolvePartition(profile!.id)).toBe(profile!.partition)
})
it('resolves default partition for null/undefined profileId', () => {
expect(browserSessionRegistry.resolvePartition(null)).toBe(ORCA_BROWSER_PARTITION)
expect(browserSessionRegistry.resolvePartition(undefined)).toBe(ORCA_BROWSER_PARTITION)
})
it('resolves default partition for unknown profileId', () => {
expect(browserSessionRegistry.resolvePartition('nonexistent')).toBe(ORCA_BROWSER_PARTITION)
})
it('strictly resolves known profile partitions without downgrading unknown profiles', () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Strict Resolve')
expect(profile).not.toBeNull()
expect(browserSessionRegistry.resolveKnownPartition(null)).toBe(ORCA_BROWSER_PARTITION)
expect(browserSessionRegistry.resolveKnownPartition(undefined)).toBe(ORCA_BROWSER_PARTITION)
expect(browserSessionRegistry.resolveKnownPartition('default')).toBe(ORCA_BROWSER_PARTITION)
expect(browserSessionRegistry.resolveKnownPartition(profile!.id)).toBe(profile!.partition)
expect(browserSessionRegistry.resolveKnownPartition('missing-profile')).toBeNull()
})
it('lists all profiles', () => {
const before = browserSessionRegistry.listProfiles().length
browserSessionRegistry.createProfile('isolated', 'List Test')
const after = browserSessionRegistry.listProfiles()
expect(after.length).toBe(before + 1)
})
it('updates profile source', () => {
const profile = browserSessionRegistry.createProfile('imported', 'Source Test')
expect(profile).not.toBeNull()
const updated = browserSessionRegistry.updateProfileSource(profile!.id, {
browserFamily: 'edge',
importedAt: Date.now()
})
expect(updated).not.toBeNull()
expect(updated!.source?.browserFamily).toBe('edge')
})
it('updates profile source with comet family', () => {
const profile = browserSessionRegistry.createProfile('imported', 'Comet Source Test')
expect(profile).not.toBeNull()
const updated = browserSessionRegistry.updateProfileSource(profile!.id, {
browserFamily: 'comet',
importedAt: Date.now()
})
expect(updated).not.toBeNull()
expect(updated!.source?.browserFamily).toBe('comet')
})
it('deletes a non-default profile', async () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Delete Test')
expect(profile).not.toBeNull()
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(true)
const deleted = await browserSessionRegistry.deleteProfile(profile!.id)
expect(deleted).toBe(true)
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(false)
expect(browserSessionRegistry.getProfile(profile!.id)).toBeNull()
})
it('clears session policy callbacks when deleting a profile', async () => {
const profile = browserSessionRegistry.createProfile('isolated', 'Policy Delete Test')
expect(profile).not.toBeNull()
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
const downloadHandler = mockSession.on.mock.calls.find(
([eventName]) => eventName === 'will-download'
)?.[1]
await expect(browserSessionRegistry.deleteProfile(profile!.id)).resolves.toBe(true)
expect(mockSession.removeListener).toHaveBeenCalledWith('will-download', downloadHandler)
expect(mockSession.setPermissionRequestHandler).toHaveBeenLastCalledWith(null)
expect(mockSession.setPermissionCheckHandler).toHaveBeenLastCalledWith(null)
expect(mockSession.setDevicePermissionHandler).toHaveBeenLastCalledWith(null)
expect(mockSession.setDisplayMediaRequestHandler).toHaveBeenLastCalledWith(null)
})
it('refuses to delete the default profile', async () => {
const deleted = await browserSessionRegistry.deleteProfile('default')
expect(deleted).toBe(false)
expect(browserSessionRegistry.getDefaultProfile()).not.toBeNull()
})
it('hydrates profiles from persisted data', () => {
const fakeProfile = {
id: '00000000-0000-0000-0000-000000000001',
scope: 'imported' as const,
partition: 'persist:orca-browser-session-00000000-0000-0000-0000-000000000001',
label: 'Hydrated',
source: { browserFamily: 'manual' as const, importedAt: 1000 }
}
browserSessionRegistry.hydrateFromPersisted([fakeProfile])
expect(browserSessionRegistry.getProfile('00000000-0000-0000-0000-000000000001')).not.toBeNull()
expect(browserSessionRegistry.isAllowedPartition(fakeProfile.partition)).toBe(true)
})
it('sets up session policies for new partitions', () => {
browserSessionRegistry.createProfile('isolated', 'Policy Test')
expect(sessionFromPartitionMock).toHaveBeenCalled()
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
expect(mockSession?.setPermissionRequestHandler).toHaveBeenCalled()
expect(mockSession?.setPermissionCheckHandler).toHaveBeenCalled()
expect(mockSession?.setDevicePermissionHandler).toHaveBeenCalled()
})
it('routes media permission requests through macOS TCC for isolated partitions', async () => {
// Why: verify the parallel fix to the default partition — isolated/imported
// profiles must also defer media permission checks to macOS instead of
// denying outright, otherwise pages inside them still hit NotAllowedError
// after the user grants Camera/Microphone to Orca.
browserSessionRegistry.createProfile('isolated', 'Media Test')
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
const requestHandler = mockSession.setPermissionRequestHandler.mock.calls[0][0]
const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0]
const cb = vi.fn()
const guestWc = { id: 7, getURL: vi.fn(() => 'https://example.com/') }
requestHandler(guestWc, 'media', cb, { mediaTypes: ['video'] })
await vi.waitFor(() => expect(cb).toHaveBeenCalledWith(true))
expect(checkHandler(null, 'media', '', { mediaType: 'video' })).toBe(true)
expect(checkHandler(null, 'notifications', '', {})).toBe(true)
expect(checkHandler(null, 'persistent-storage', '', {})).toBe(true)
expect(checkHandler(null, 'geolocation', '', {})).toBe(false)
})
it('wires WebAuthn device selection for isolated partitions', () => {
browserSessionRegistry.createProfile('isolated', 'Security Key Test')
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
const devicePermissionHandler = mockSession.setDevicePermissionHandler.mock.calls[0][0]
const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0]
expect(
devicePermissionHandler({
deviceType: 'hid',
origin: 'https://github.com',
device: { collections: [{ usagePage: 0xf1d0 }] }
})
).toBe(true)
expect(
devicePermissionHandler({
deviceType: 'hid',
origin: 'http://[::1]:5173',
device: { collections: [{ usagePage: 0xf1d0 }] }
})
).toBe(true)
expect(
devicePermissionHandler({
deviceType: 'hid',
origin: 'https://github.com',
device: { collections: [{ usagePage: 1 }] }
})
).toBe(false)
expect(checkHandler(null, 'hid', '', { securityOrigin: 'https://github.com' })).toBe(true)
const selectHidHandler = mockSession.on.mock.calls.find(
([eventName]) => eventName === 'select-hid-device'
)?.[1]
const hidCallback = vi.fn()
selectHidHandler(
{ preventDefault: vi.fn() },
{
frame: { url: 'https://github.com' },
deviceList: [
{ deviceId: 'keyboard', collections: [{ usagePage: 1 }] },
{ deviceId: 'security-key', collections: [{ usagePage: 0xf1d0 }] }
]
},
hidCallback
)
expect(hidCallback).toHaveBeenCalledWith('security-key')
const selectWebAuthnHandler = mockSession.on.mock.calls.find(
([eventName]) => eventName === 'select-webauthn-account'
)?.[1]
const webAuthnCallback = vi.fn()
selectWebAuthnHandler(
{ preventDefault: vi.fn() },
{ accounts: [{ credentialId: 'credential-1' }] },
webAuthnCallback
)
expect(webAuthnCallback).toHaveBeenCalledWith('credential-1')
})
describe('setupClientHintsOverride', () => {
it('overrides sec-ch-ua headers for Edge UA', () => {
const onBeforeSendHeaders = vi.fn()
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
const edgeUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.6890.3 Safari/537.36 Edg/147.0.3210.5'
setupClientHintsOverride(mockSess, edgeUa)
expect(onBeforeSendHeaders).toHaveBeenCalledWith(
{ urls: ['https://*/*'] },
expect.any(Function)
)
const callback = vi.fn()
const listener = onBeforeSendHeaders.mock.calls[0][1]
listener(
{ requestHeaders: { 'sec-ch-ua': 'old', 'sec-ch-ua-full-version-list': 'old' } },
callback
)
const modified = callback.mock.calls[0][0].requestHeaders
expect(modified['sec-ch-ua']).toContain('Microsoft Edge')
expect(modified['sec-ch-ua']).toContain('"147"')
expect(modified['sec-ch-ua-full-version-list']).toContain('147.0.3210.5')
})
it('overrides sec-ch-ua headers for Chrome UA', () => {
const onBeforeSendHeaders = vi.fn()
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
const chromeUa =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.6890.3 Safari/537.36'
setupClientHintsOverride(mockSess, chromeUa)
const callback = vi.fn()
const listener = onBeforeSendHeaders.mock.calls[0][1]
listener({ requestHeaders: { 'sec-ch-ua': 'old' } }, callback)
const modified = callback.mock.calls[0][0].requestHeaders
expect(modified['sec-ch-ua']).toContain('Google Chrome')
expect(modified['sec-ch-ua']).not.toContain('Microsoft Edge')
})
it('does not register handler for non-Chrome UA', () => {
const onBeforeSendHeaders = vi.fn()
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
setupClientHintsOverride(mockSess, 'Mozilla/5.0 (compatible; MSIE 10.0)')
expect(onBeforeSendHeaders).not.toHaveBeenCalled()
})
it('leaves non-Client-Hints headers unchanged', () => {
const onBeforeSendHeaders = vi.fn()
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
setupClientHintsOverride(mockSess, 'Mozilla/5.0 Chrome/147.0.0.0 Safari/537.36')
const callback = vi.fn()
const listener = onBeforeSendHeaders.mock.calls[0][1]
listener(
{ requestHeaders: { Cookie: 'abc=123', 'sec-ch-ua': 'old', Accept: 'text/html' } },
callback
)
const modified = callback.mock.calls[0][0].requestHeaders
expect(modified.Cookie).toBe('abc=123')
expect(modified.Accept).toBe('text/html')
})
})
})