Files
orca/src/main/runtime/orca-runtime-browser-client-hosted.test.ts
T
Neil 121eb3bc84 fix(runtime): resolve folder workspaces wherever a command can be given one (#35223)
* fix(runtime): resolve workspaces, not only Git worktrees, wherever a folder id can arrive

The Git-only selector resolver never matches project-group folder
workspaces (no repo row), so worktree show/sleep, terminal list by
workspace key, the active-terminal fallback, scrollback reads, Kitty file
reads, Linear --current, browser commands, plugin context and the session
tab fallbacks failed with selector_not_found for them. Route those
callers through resolveWorkspaceSelector and rename the Git-only resolver
to resolveGitWorktreeSelector so it reads as the repo-backed catalog only.

Refs STA-12562

* test(runtime): follow the workspace resolver rename in host fakes

Refs STA-12562

* test(runtime): keep partial browser host fakes and mail-routing stubs on the renamed resolver

Refs STA-12562

* test(runtime): drop an accidental copy of the folder selector suite

Refs STA-12562

* fix(runtime): keep folder workspaces off the skill worktree fence and the Linear meta backfill

Skill installs fence a worktreeId by repo rows, so a folder id there must
come through folderWorkspaceId, where its own host fence applies. Linear
--current answers a folder's legacy link without writing a worktreeMeta
row keyed by the folder id.

Refs STA-12562
2026-10-28 10:52:37 -07:00

706 lines
26 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentBrowserBridge } from '../browser/agent-browser-bridge'
import type { RuntimeBrowserCommandHost } from './browser/browser-command-contracts'
import { RuntimeBrowserPageRegistry } from './runtime-browser-page-registry'
const { startBrowserScreencastMock, browserSessionRegistryMock } = vi.hoisted(() => ({
startBrowserScreencastMock: vi.fn(),
browserSessionRegistryMock: {
profiles: new Map([
[
'default',
{
id: 'default',
scope: 'default',
partition: 'persist:orca-browser',
label: 'Default',
source: null
}
]
]),
getDefaultProfile: vi.fn(),
getProfile: vi.fn(),
resolveKnownPartition: vi.fn(),
createProfile: vi.fn()
}
}))
vi.mock('electron', () => ({
ipcMain: { on: vi.fn(), removeListener: vi.fn() },
webContents: { fromId: vi.fn() }
}))
vi.mock('../browser/browser-screencast-stream', () => ({
startBrowserScreencast: startBrowserScreencastMock
}))
vi.mock('../ipc/browser-tab-registration-wait', () => ({
waitForTabRegistration: vi.fn(),
waitForWorktreeTabRegistration: vi.fn()
}))
vi.mock('../browser/browser-session-registry', () => ({
browserSessionRegistry: browserSessionRegistryMock
}))
function deferred<T>() {
let resolve!: (value: T) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
function createHost(overrides: Partial<RuntimeBrowserCommandHost> = {}): RuntimeBrowserCommandHost {
const runtimeBrowserPages = new RuntimeBrowserPageRegistry()
const bridge = overrides.getAgentBrowserBridge
? overrides.getAgentBrowserBridge()
: ({
getRegisteredTabs: vi.fn(() => new Map([['page-1', 100]])),
getActivePageId: vi.fn(() => 'page-1'),
tabList: vi.fn(() => ({
tabs: [
{
browserPageId: 'page-1',
index: 0,
url: 'about:blank',
title: 'Browser',
active: true
}
]
}))
} as unknown as AgentBrowserBridge)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial host fake; these tests reach only the methods it defines.
return {
resolveWorkspaceSelector: async (selector) => ({ id: selector.replace(/^id:/, '') }),
resolveBrowserWorkspace: async (selector) => ({ id: selector.replace(/^id:/, '') }),
getRuntimeBrowserPageRegistry: () => runtimeBrowserPages,
getAuthoritativeWindow: vi.fn(),
getAvailableAuthoritativeWindow: vi.fn(() => null),
getOffscreenBrowserBackend: vi.fn(() => null),
...overrides,
getAgentBrowserBridge: () => bridge
} as unknown as RuntimeBrowserCommandHost
}
function publishClientPage(
registry: RuntimeBrowserPageRegistry,
overrides: { browserPageId: string; workspaceId?: string; active?: boolean }
): void {
registry.publishClientPage({
browserPageId: overrides.browserPageId,
workspaceId: overrides.workspaceId ?? 'wt-1',
browserProfileId: 'default',
executionHostKey: 'native:runtime-a:7',
placement: {
kind: 'client',
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
},
url: 'https://client.test/',
title: 'Client page',
loading: false,
active: overrides.active ?? true
})
}
describe('RuntimeBrowserCommands client-hosted routing', () => {
beforeEach(() => {
startBrowserScreencastMock.mockReset()
browserSessionRegistryMock.getDefaultProfile.mockReset()
browserSessionRegistryMock.getDefaultProfile.mockImplementation(() =>
browserSessionRegistryMock.profiles.get('default')
)
browserSessionRegistryMock.getProfile.mockReset()
browserSessionRegistryMock.getProfile.mockImplementation(
(profileId: string) => browserSessionRegistryMock.profiles.get(profileId) ?? null
)
browserSessionRegistryMock.resolveKnownPartition.mockReset()
browserSessionRegistryMock.resolveKnownPartition.mockImplementation(
(profileId: string | null | undefined) => {
if (!profileId) {
return 'persist:orca-browser'
}
return browserSessionRegistryMock.profiles.get(profileId)?.partition ?? null
}
)
})
it('creates an explicitly client-placed page without a server renderer or offscreen backend', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const createClientPage = vi.fn(async () => ({
kind: 'client' as const,
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
}))
const issueClientPageCommand = vi.fn(() => ({
event: {} as never,
result: Promise.resolve({ status: 'completed' as const })
}))
const resolveBrowserNetworkExecutionHost = vi.fn(async () => ({
kind: 'native' as const,
runtimeId: 'runtime-a',
revision: 7
}))
const offscreenCreate = vi.fn()
const send = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
getAvailableAuthoritativeWindow: vi.fn(() => null),
getAuthoritativeWindow: vi.fn(() => ({ webContents: { send } }) as never),
getOffscreenBrowserBackend: vi.fn(
() => ({ createTab: offscreenCreate }) as unknown as never
),
resolveBrowserNetworkExecutionHost,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage,
issueClientPageCommand
}) as never
})
)
await expect(
commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-stable',
url: 'https://remote.internal/',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
).resolves.toEqual({ browserPageId: 'page-stable' })
expect(resolveBrowserNetworkExecutionHost).toHaveBeenCalledWith({ id: 'wt-1' })
expect(createClientPage).toHaveBeenCalledWith({
browserPageId: 'page-stable',
browserHostClientId: 'host-a',
pairedDeviceId: 'device-a',
browserProfileId: 'default',
executionHostKey: JSON.stringify(['native', 'runtime-a', 7]),
requiredCapabilities: ['automation-v1'],
// Clients need workspace identity to restore page inventory after restart.
workspaceId: 'wt-1'
})
expect(issueClientPageCommand).toHaveBeenCalledWith(
expect.objectContaining({ browserPageId: 'page-stable' }),
{ type: 'navigate', url: 'https://remote.internal/' }
)
expect(offscreenCreate).not.toHaveBeenCalled()
expect(send).not.toHaveBeenCalled()
})
it('moves a user-created client page into the clicked split group', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const markHeadlessBrowserSessionTabActive = vi.fn()
const host = createHost({
resolveBrowserNetworkExecutionHost: vi.fn(async () => ({
kind: 'native' as const,
runtimeId: 'runtime-a',
revision: 7
})),
markHeadlessBrowserSessionTabActive,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage: vi.fn(async () => ({
kind: 'client' as const,
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
})),
issueClientPageCommand: vi.fn(() => ({
event: {} as never,
result: Promise.resolve({ status: 'completed' as const })
}))
}) as never
})
const commands = new RuntimeBrowserCommands(host)
await commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-grouped',
activate: true,
targetGroupId: 'group-right',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
expect(markHeadlessBrowserSessionTabActive).toHaveBeenCalledWith('wt-1', 'page-grouped', {
targetGroupId: 'group-right',
focusesHost: true,
caller: { clientNavigationId: 'device-a', navigation: 'all' }
})
markHeadlessBrowserSessionTabActive.mockClear()
await commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-background',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
expect(markHeadlessBrowserSessionTabActive).not.toHaveBeenCalled()
})
it('keeps a background client page out of the workspace current-page slot', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
const commands = new RuntimeBrowserCommands(
createHost({
resolveBrowserNetworkExecutionHost: vi.fn(async () => ({
kind: 'native' as const,
runtimeId: 'runtime-a',
revision: 7
})),
getRuntimeBrowserPageRegistry: () => registry,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage: vi.fn(async () => ({
kind: 'client' as const,
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
})),
issueClientPageCommand: vi.fn(() => ({
event: {} as never,
result: Promise.resolve({ status: 'completed' as const })
}))
}) as never
})
)
// browser.tabCurrent uses active; background creates must preserve the user's selected page.
await commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-background',
activate: false,
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
expect(registry.getPage('page-background')).toMatchObject({ active: false })
})
it('publishes the proven client page before navigation and scopes it to the worktree', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const createProof = deferred<{
kind: 'client'
browserHostClientId: string
browserHostGeneration: number
pageHostGeneration: number
}>()
const navigationProof = deferred<{ status: 'completed' }>()
const registry = new RuntimeBrowserPageRegistry()
const order: string[] = []
const publishClientPage = vi
.spyOn(registry, 'publishClientPage')
.mockImplementation((input) => {
order.push('publish')
return RuntimeBrowserPageRegistry.prototype.publishClientPage.call(registry, input)
})
const notifyHeadlessBrowserSessionTabsChanged = vi.fn(() => order.push('notify'))
const issueClientPageCommand = vi.fn(() => {
order.push('navigate')
return { event: {} as never, result: navigationProof.promise }
})
const commands = new RuntimeBrowserCommands(
createHost({
resolveBrowserWorkspace: vi.fn(async () => ({ id: 'wt-1' })),
resolveBrowserNetworkExecutionHost: vi.fn(async () => ({
kind: 'native' as const,
runtimeId: 'runtime-a',
revision: 7
})),
getRuntimeBrowserPageRegistry: () => registry,
notifyHeadlessBrowserSessionTabsChanged,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage: vi.fn(() => createProof.promise),
issueClientPageCommand
}) as never
} as unknown as Partial<RuntimeBrowserCommandHost>)
)
const creation = commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-stable',
url: 'https://remote.internal/',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
expect(registry.listPages('wt-1')).toEqual([])
expect(issueClientPageCommand).not.toHaveBeenCalled()
createProof.resolve({
kind: 'client',
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
})
await vi.waitFor(() => expect(issueClientPageCommand).toHaveBeenCalledOnce())
expect(order).toEqual(['publish', 'notify', 'navigate'])
expect(publishClientPage).toHaveBeenCalledWith(
expect.objectContaining({
browserPageId: 'page-stable',
workspaceId: 'wt-1',
browserProfileId: 'default',
url: 'about:blank',
active: true
})
)
navigationProof.resolve({ status: 'completed' })
await expect(creation).resolves.toEqual({ browserPageId: 'page-stable' })
expect(registry.getPage('page-stable')).toMatchObject({
url: 'https://remote.internal/',
loading: false
})
})
it('retains a folder-scoped logical client page when navigation fails', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
const resolveBrowserWorkspace = vi.fn(async () => ({
id: 'folder:folder-1',
hostId: 'ssh:target-a' as const
}))
const resolveBrowserNetworkExecutionHost = vi.fn(async () => ({
kind: 'ssh' as const,
targetId: 'target-a',
providerEpoch: 'provider-a',
connectionGeneration: 4
}))
const offscreenCreate = vi.fn()
const send = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
resolveBrowserWorkspace,
resolveBrowserNetworkExecutionHost,
getRuntimeBrowserPageRegistry: () => registry,
getAvailableAuthoritativeWindow: vi.fn(() => null),
getAuthoritativeWindow: vi.fn(() => ({ webContents: { send } }) as never),
getOffscreenBrowserBackend: vi.fn(
() => ({ createTab: offscreenCreate }) as unknown as never
),
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage: vi.fn(async () => ({
kind: 'client' as const,
browserHostClientId: 'host-a',
browserHostGeneration: 3,
pageHostGeneration: 9
})),
issueClientPageCommand: vi.fn(() => ({
event: {} as never,
result: Promise.resolve({
status: 'failed' as const,
errorCode: 'navigation_failed'
})
}))
}) as never
} as unknown as Partial<RuntimeBrowserCommandHost>)
)
await expect(
commands.browserTabCreate(
{
worktree: 'id:folder:folder-1',
page: 'page-folder',
url: 'https://remote.internal/',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
).resolves.toEqual({ browserPageId: 'page-folder' })
expect(resolveBrowserWorkspace).toHaveBeenCalledWith('id:folder:folder-1')
expect(resolveBrowserNetworkExecutionHost).toHaveBeenCalledWith(
expect.objectContaining({ id: 'folder:folder-1', hostId: 'ssh:target-a' })
)
expect(registry.getPage('page-folder')).toMatchObject({
workspaceId: 'folder:folder-1',
url: 'about:blank',
loading: false
})
expect(offscreenCreate).not.toHaveBeenCalled()
expect(send).not.toHaveBeenCalled()
})
it('lists, shows, and resolves the current client page on a browserless host', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-client', workspaceId: 'folder:folder-1' })
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => null,
getRuntimeBrowserPageRegistry: () => registry,
resolveBrowserWorkspace: vi.fn(async () => ({ id: 'folder:folder-1' }))
})
)
await expect(
commands.browserTabList({ worktree: 'id:folder:folder-1' })
).resolves.toMatchObject({
tabs: [
{
browserPageId: 'page-client',
index: 0,
url: 'https://client.test/',
title: 'Client page',
active: true,
worktreeId: 'folder:folder-1'
}
]
})
await expect(
commands.browserTabShow({ worktree: 'id:folder:folder-1', page: 'page-client' })
).resolves.toMatchObject({ tab: { browserPageId: 'page-client' } })
await expect(
commands.browserTabCurrent({ worktree: 'id:folder:folder-1' })
).resolves.toMatchObject({ tab: { browserPageId: 'page-client' } })
})
it('switches logical client pages without invoking the server bridge', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-a', active: true })
publishClientPage(registry, { browserPageId: 'page-b', active: false })
const notify = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => null,
getRuntimeBrowserPageRegistry: () => registry,
resolveBrowserWorkspace: vi.fn(async () => ({ id: 'wt-1' })),
notifyHeadlessBrowserSessionTabsChanged: notify
})
)
await expect(
commands.browserTabSwitch({ worktree: 'id:wt-1', page: 'page-b' })
).resolves.toEqual({ switched: 1, browserPageId: 'page-b' })
expect(registry.getPage('page-a')?.active).toBe(false)
expect(registry.getPage('page-b')?.active).toBe(true)
expect(notify).toHaveBeenCalledWith('wt-1')
})
it('reports one global active client page across independently active workspaces', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-a', workspaceId: 'wt-1' })
publishClientPage(registry, { browserPageId: 'page-b', workspaceId: 'wt-2' })
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => null,
getRuntimeBrowserPageRegistry: () => registry
})
)
await expect(commands.browserTabCurrent({})).resolves.toMatchObject({
tab: { browserPageId: 'page-b' }
})
await expect(commands.browserTabList({})).resolves.toMatchObject({
tabs: [
{ browserPageId: 'page-a', active: false },
{ browserPageId: 'page-b', active: true }
]
})
await expect(commands.browserTabCurrent({ worktree: 'id:wt-1' })).resolves.toMatchObject({
tab: { browserPageId: 'page-a' }
})
})
it('makes a switched server tab globally active without clearing other workspace scopes', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-client-a', workspaceId: 'wt-1' })
publishClientPage(registry, { browserPageId: 'page-client-b', workspaceId: 'wt-2' })
const tabSwitch = vi.fn(async () => ({ switched: 0, browserPageId: 'page-server' }))
const bridge = {
getRegisteredTabs: vi.fn(() => new Map([['page-server', 100]])),
tabList: vi.fn(() => ({
tabs: [
{
browserPageId: 'page-server',
index: 0,
url: 'about:blank',
title: 'Server page',
active: true
}
]
})),
tabSwitch
} as unknown as AgentBrowserBridge
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => bridge,
getRuntimeBrowserPageRegistry: () => registry
})
)
await expect(commands.browserTabSwitch({ page: 'page-server' })).resolves.toMatchObject({
browserPageId: 'page-server'
})
expect(registry.listPages().every((page) => !page.active)).toBe(true)
expect(registry.listPages('wt-1')[0]?.active).toBe(true)
expect(registry.listPages('wt-2')[0]?.active).toBe(true)
await expect(commands.browserTabCurrent({})).resolves.toMatchObject({
tab: { browserPageId: 'page-server' }
})
})
it('closes the exact client page and excludes it from server screencast', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-client' })
const issueClientPageCommand = vi.fn(() => ({
event: {} as never,
result: Promise.resolve({ status: 'completed' as const })
}))
const beginPageRetirement = vi.fn((browserPageId, placement) => ({
browserPageId,
placement
}))
const requireClientPage = vi.fn(() => registry.getPage('page-client')!.placement)
const completePageRetirement = vi.fn(() => true)
const retireRuntimeOwnedBrowserSessionTab = vi.fn()
const notifyHeadlessBrowserSessionTabsChanged = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => null,
getRuntimeBrowserPageRegistry: () => registry,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
issueClientPageCommand,
requireClientPage,
beginPageRetirement,
completePageRetirement,
getPlacement: () => registry.getPage('page-client')?.placement
}) as never,
resolveBrowserWorkspace: vi.fn(async () => ({ id: 'wt-1' })),
retireRuntimeOwnedBrowserSessionTab,
notifyHeadlessBrowserSessionTabsChanged
})
)
await expect(
commands.browserScreencast({ page: 'page-client', format: 'png' }, { sendBinary: vi.fn() })
).rejects.toThrow('do not support server screencast')
expect(startBrowserScreencastMock).not.toHaveBeenCalled()
await expect(
commands.browserTabClose({ worktree: 'id:wt-1', page: 'page-client' })
).resolves.toEqual({ closed: true })
expect(issueClientPageCommand).toHaveBeenCalledWith(
expect.objectContaining({ browserPageId: 'page-client' }),
expect.objectContaining({ type: 'closePage' })
)
expect(registry.getPage('page-client')).toBeUndefined()
expect(retireRuntimeOwnedBrowserSessionTab).toHaveBeenCalledWith('wt-1', 'page-client')
expect(notifyHeadlessBrowserSessionTabsChanged).not.toHaveBeenCalled()
})
it('closes a retained client page whose host is gone without commanding it', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const registry = new RuntimeBrowserPageRegistry()
publishClientPage(registry, { browserPageId: 'page-client' })
const issueClientPageCommand = vi.fn()
const requireClientPage = vi.fn(() => {
throw new Error('browser_host_lease_required')
})
const retireRuntimeOwnedBrowserSessionTab = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
getAgentBrowserBridge: () => null,
getRuntimeBrowserPageRegistry: () => registry,
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
issueClientPageCommand,
requireClientPage,
beginPageRetirement: vi.fn(),
completePageRetirement: vi.fn(() => true),
// The fence released the placement while keeping the page listed.
getPlacement: () => undefined
}) as never,
resolveBrowserWorkspace: vi.fn(async () => ({ id: 'wt-1' })),
retireRuntimeOwnedBrowserSessionTab,
notifyHeadlessBrowserSessionTabsChanged: vi.fn()
})
)
await expect(
commands.browserTabClose({ worktree: 'id:wt-1', page: 'page-client' })
).resolves.toEqual({ closed: true })
expect(issueClientPageCommand).not.toHaveBeenCalled()
expect(requireClientPage).not.toHaveBeenCalled()
expect(registry.getPage('page-client')).toBeUndefined()
expect(retireRuntimeOwnedBrowserSessionTab).toHaveBeenCalledWith('wt-1', 'page-client')
})
it('does not fall back to a server page after explicit client placement fails', async () => {
const { RuntimeBrowserCommands } = await import('./browser/browser-commands')
const offscreenCreate = vi.fn()
const send = vi.fn()
const commands = new RuntimeBrowserCommands(
createHost({
getAvailableAuthoritativeWindow: vi.fn(() => null),
getAuthoritativeWindow: vi.fn(() => ({ webContents: { send } }) as never),
getOffscreenBrowserBackend: vi.fn(
() => ({ createTab: offscreenCreate }) as unknown as never
),
resolveBrowserNetworkExecutionHost: vi.fn(async () => ({
kind: 'native' as const,
runtimeId: 'runtime-a',
revision: 7
})),
getBrowserHostLeaseRegistry: () =>
({
authorityRuntimeId: 'runtime-a',
authorityEpoch: 'epoch-a',
createClientPage: vi.fn(async () => {
throw new Error('browser_client_page_mount_failed')
}),
issueClientPageCommand: vi.fn()
}) as never
})
)
await expect(
commands.browserTabCreate(
{
worktree: 'id:wt-1',
page: 'page-stable',
placement: { kind: 'client', browserHostClientId: 'host-a' }
},
{ pairedDeviceId: 'device-a' }
)
).rejects.toThrow('browser_client_page_mount_failed')
expect(offscreenCreate).not.toHaveBeenCalled()
expect(send).not.toHaveBeenCalled()
})
})