mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 00:03:52 +00:00
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:
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user