feat(browser): add an unexposed profile reset operation

The existing clear action removes cookies only, leaving the rest of the
partition and broader browsing state intact. Add an internal reset that uses
Electron's BrowsingDataRemover-backed clearData path, removes staged import
files and source metadata, then reloads every live context in the target
Session.

Serialize the operation with cookie imports through the existing per-Session
mutation lock. Keep it deliberately unreachable from UI, IPC, RPC, and preload
surfaces until the remaining import source-metadata race is closed alongside
the destructive action's exposure.
This commit is contained in:
Brennan Benson
2026-08-19 17:11:10 -07:00
parent fcdbcf85d0
commit 15e2b310d4
2 changed files with 201 additions and 0 deletions
@@ -0,0 +1,158 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Mocked } from 'vitest'
import type { Session } from 'electron'
const getAllWebContentsMock = vi.fn()
vi.mock('electron', () => ({
webContents: { getAllWebContents: () => getAllWebContentsMock() }
}))
import { findPartitionWebContents, resetBrowserProfilePartition } from './browser-profile-reset'
import { acquireCookieMutationLock, withCookieMutationLock } from './browser-cookie-import-clear'
type SessionMock = Session & Mocked<Pick<Session, 'clearData'>>
function makeSession(overrides: Partial<SessionMock> = {}): SessionMock {
return {
clearData: vi.fn<Session['clearData']>(async () => undefined),
...overrides
} as SessionMock
}
function makeContents(session: unknown, opts: { destroyed?: boolean; onReload?: () => void } = {}) {
return {
session,
isDestroyed: () => opts.destroyed === true,
reload: vi.fn(() => opts.onReload?.())
}
}
describe('resetBrowserProfilePartition', () => {
beforeEach(() => {
getAllWebContentsMock.mockReset()
getAllWebContentsMock.mockReturnValue([])
})
it('clears the whole partition, not a filtered subset', async () => {
const targetSession = makeSession()
await resetBrowserProfilePartition({
targetSession,
partition: 'persist:orca-browser',
clearPendingCookieImport: vi.fn(),
clearImportedSource: vi.fn()
})
expect(targetSession.clearData).toHaveBeenCalledTimes(1)
expect(targetSession.clearData).toHaveBeenCalledWith()
})
it('forwards non-default partitions to staged-import deletion', async () => {
const partition = 'persist:orca-browser-session-imported'
const clearPendingCookieImport = vi.fn()
await resetBrowserProfilePartition({
targetSession: makeSession(),
partition,
clearPendingCookieImport,
clearImportedSource: vi.fn()
})
expect(clearPendingCookieImport).toHaveBeenCalledWith(partition)
})
it('clears the imported-source badge before the data is gone', async () => {
const order: string[] = []
const clearImportedSource = vi.fn(() => void order.push('source'))
const targetSession = makeSession({
clearData: vi.fn(async () => void order.push('clearData'))
})
await resetBrowserProfilePartition({
targetSession,
partition: 'persist:orca-browser',
clearPendingCookieImport: vi.fn(() => void order.push('staged')),
clearImportedSource
})
expect(clearImportedSource).toHaveBeenCalledTimes(1)
expect(order).toEqual(['source', 'staged', 'clearData'])
})
it('reloads every live context in the partition after clearing', async () => {
const order: string[] = []
const targetSession = makeSession({
clearData: vi.fn(async () => void order.push('clearData'))
})
const other = makeSession()
const mine = makeContents(targetSession, { onReload: () => void order.push('mine') })
const popup = makeContents(targetSession, { onReload: () => void order.push('popup') })
const offscreen = makeContents(targetSession, { onReload: () => void order.push('offscreen') })
const foreign = makeContents(other)
const dead = makeContents(targetSession, { destroyed: true })
getAllWebContentsMock.mockReturnValue([mine, popup, offscreen, foreign, dead])
await resetBrowserProfilePartition({
targetSession,
partition: 'persist:orca-browser',
clearPendingCookieImport: vi.fn(),
clearImportedSource: vi.fn()
})
expect(mine.reload).toHaveBeenCalledTimes(1)
expect(popup.reload).toHaveBeenCalledTimes(1)
expect(offscreen.reload).toHaveBeenCalledTimes(1)
expect(foreign.reload).not.toHaveBeenCalled()
expect(dead.reload).not.toHaveBeenCalled()
expect(order).toEqual(['clearData', 'mine', 'popup', 'offscreen'])
})
it('selects contexts by session identity so popups and offscreen guests are included', () => {
const targetSession = makeSession()
const other = makeSession()
const a = makeContents(targetSession)
const b = makeContents(other)
getAllWebContentsMock.mockReturnValue([a, b])
expect(findPartitionWebContents(targetSession)).toEqual([a])
})
it('survives a context that dies mid-reset', async () => {
const targetSession = makeSession()
const exploding = {
session: targetSession,
isDestroyed: () => false,
reload: vi.fn(() => {
throw new Error('destroyed')
})
}
const healthy = makeContents(targetSession)
getAllWebContentsMock.mockReturnValue([exploding, healthy])
await expect(
resetBrowserProfilePartition({
targetSession,
partition: 'persist:orca-browser',
clearPendingCookieImport: vi.fn(),
clearImportedSource: vi.fn()
})
).resolves.toBeUndefined()
expect(healthy.reload).toHaveBeenCalledTimes(1)
})
it('waits for the session mutation lock and releases it after failure', async () => {
const targetSession = makeSession({
clearData: vi.fn(async () => {
throw new Error('clear failed')
})
})
const clearImportedSource = vi.fn()
const releaseImport = await acquireCookieMutationLock(targetSession)
const reset = resetBrowserProfilePartition({
targetSession,
partition: 'persist:orca-browser',
clearPendingCookieImport: vi.fn(),
clearImportedSource
})
await Promise.resolve()
expect(clearImportedSource).not.toHaveBeenCalled()
releaseImport()
await expect(reset).rejects.toThrow('clear failed')
await expect(withCookieMutationLock(targetSession, async () => 'released')).resolves.toBe(
'released'
)
})
})
+43
View File
@@ -0,0 +1,43 @@
import { webContents } from 'electron'
import type { Session, WebContents } from 'electron'
import { withCookieMutationLock } from './browser-cookie-import-clear'
export type BrowserProfileResetDependencies = {
targetSession: Session
partition: string
clearPendingCookieImport: (partition: string) => void
clearImportedSource: () => void
}
// Electron's global list includes popups and offscreen guests omitted by Orca's tab registry.
export function findPartitionWebContents(targetSession: Session): WebContents[] {
return webContents
.getAllWebContents()
.filter((contents) => !contents.isDestroyed() && contents.session === targetSession)
}
function reloadPartitionContexts(targetSession: Session): void {
for (const contents of findPartitionWebContents(targetSession)) {
try {
contents.reload()
} catch {
// A context that dies between enumeration and reload needs no reload.
}
}
}
/** Resets one session partition while serializing against cookie import mutations. */
export async function resetBrowserProfilePartition(
deps: BrowserProfileResetDependencies
): Promise<void> {
const { targetSession, partition, clearPendingCookieImport, clearImportedSource } = deps
await withCookieMutationLock(targetSession, async () => {
// Clear metadata first so a partial reset never advertises an import whose data may be gone.
clearImportedSource()
clearPendingCookieImport(partition)
// clearStorageData bypasses trust-token, bounce-tracking, and network-history removal.
await targetSession.clearData()
reloadPartitionContexts(targetSession)
})
}