Files
orca/tests/e2e/paired-remote-browser-link-open-routing.spec.ts
T
Jinjing 939acf1c2b Fix remote server browser tab fallback behavior (#14194)
* fix(browser): require runtime browser capability

* fix(browser): reuse capability verdict and exclude floating terminals

Cache the runtime browser capability check across multiple remote port opens instead of checking for each port independently. Prevent floating-terminal workspaces from routing to remote browsers.

* fix(browser): pin remote pane link opens to owning runtime

Remote pane link opens must route to the pane's runtime, never the client.
Fails if the workspace has moved to a different host while the pane still
displays a remote page, preventing silent misrouting to a dev server.

* trim comments
2026-08-13 00:12:20 -07:00

311 lines
12 KiB
TypeScript

import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
import type { AddressInfo } from 'node:net'
import type { Page } from '@stablyai/playwright-test'
import { LOCAL_EXECUTION_HOST_ID } from '../../src/shared/execution-host'
import {
launchHeadlessPairedRuntimeHost,
type HeadlessPairedRuntimeHost
} from './helpers/headless-paired-runtime-host'
import { expect, test } from './helpers/orca-app'
import {
launchPairedElectronClient,
type PairedElectronClient
} from './helpers/paired-electron-client'
// The link is a dev-server URL, so a client-local fallback would silently load a *different
// machine's* server while looking successful.
const PANE_PATH = '/remote-pane'
const LINK_PATH = '/remote-link-target'
const LINK_MARKER = 'remote link target'
type LinkFixtureServer = {
close: () => Promise<void>
linkLoadCount: () => number
linkUrl: string
paneUrl: string
}
async function startLinkFixtureServer(): Promise<LinkFixtureServer> {
let linkLoadCount = 0
const server: Server = createServer((request: IncomingMessage, response: ServerResponse) => {
const requestPath = request.url ?? '/'
if (requestPath.startsWith(LINK_PATH)) {
linkLoadCount += 1
response.writeHead(200, {
'cache-control': 'no-store',
'content-type': 'text/html; charset=utf-8'
})
response.end(`<!doctype html><html><body><h1>${LINK_MARKER}</h1></body></html>`)
return
}
if (requestPath.startsWith(PANE_PATH)) {
response.writeHead(200, {
'cache-control': 'no-store',
'content-type': 'text/html; charset=utf-8'
})
// Why the viewport-filling anchor: the context menu reads the link with
// elementFromPoint over the screencast, so covering the viewport keeps the test off
// screencast coordinate mapping — any right-click lands on the link.
response.end(
`<!doctype html><html><body style="margin:0"><a href="${LINK_PATH}" style="position:fixed;inset:0;display:block;background:#fff;color:#000;font:24px sans-serif">open me</a></body></html>`
)
return
}
response.writeHead(404, { 'content-type': 'text/plain' })
response.end('not found')
})
await new Promise<void>((resolve, reject) => {
server.once('error', reject)
server.listen(0, '127.0.0.1', () => {
server.off('error', reject)
resolve()
})
})
const { port } = server.address() as AddressInfo
const origin = `http://127.0.0.1:${port}`
return {
close: () =>
new Promise<void>((resolve, reject) => {
// Keep-alive sockets from either browser would otherwise hold the close open.
server.closeAllConnections()
server.close((error) => (error ? reject(error) : resolve()))
}),
linkLoadCount: () => linkLoadCount,
linkUrl: `${origin}${LINK_PATH}`,
paneUrl: `${origin}${PANE_PATH}`
}
}
/** Asked over the host's own connection, not proxied through the client under test. */
async function readHostBrowserUrls(
host: HeadlessPairedRuntimeHost,
worktreeId: string
): Promise<string[]> {
const response = await host.client.call<{ tabs: { type: string; url?: string }[] }>(
'session.tabs.list',
{ worktree: `id:${worktreeId}` },
{ timeoutMs: 15_000 }
)
return response.result.tabs.filter((tab) => tab.type === 'browser').map((tab) => tab.url ?? '')
}
/** A remote pane is a screencast image; a <webview> means the page really loaded on this machine. */
async function readLocalBrowserViewUrls(page: Page): Promise<string[]> {
return page.evaluate(() =>
Array.from(document.querySelectorAll('webview')).map(
(view) => (view as HTMLElement & { src?: string }).src || view.getAttribute('src') || ''
)
)
}
async function readRemotePaneUrls(page: Page, worktreeId: string): Promise<string[]> {
return page.evaluate((worktreeId) => {
const state = window.__store?.getState()
const workspaces = state?.browserTabsByWorktree[worktreeId] ?? []
return workspaces
.flatMap((workspace) => state?.browserPagesByWorkspace[workspace.id] ?? [])
.filter(
(browserPage) => state?.remoteBrowserPageHandlesByPageId[browserPage.id]?.environmentId
)
.map((browserPage) => browserPage.url)
}, worktreeId)
}
/** The client mirrors host browser tabs on its own; this finds the mirrored page for one URL. */
async function findMirroredPage(
page: Page,
worktreeId: string,
url: string
): Promise<{ handleEnvironmentId: string | null; pageId: string } | null> {
return page.evaluate(
({ url, worktreeId }) => {
const state = window.__store?.getState()
for (const workspace of state?.browserTabsByWorktree[worktreeId] ?? []) {
for (const browserPage of state?.browserPagesByWorkspace[workspace.id] ?? []) {
if (browserPage.url.startsWith(url)) {
return {
handleEnvironmentId:
state?.remoteBrowserPageHandlesByPageId[browserPage.id]?.environmentId ?? null,
pageId: browserPage.id
}
}
}
}
return null
},
{ url, worktreeId }
)
}
async function focusMirroredPage(page: Page, worktreeId: string, pageId: string): Promise<void> {
await page.evaluate(
({ pageId, worktreeId }) => {
window.__store?.getState().focusBrowserTabInWorktree(worktreeId, pageId, {
surfacePane: true
})
},
{ pageId, worktreeId }
)
}
type LinkOpenOutcome = 'opened on this machine' | 'pending' | 'refused'
async function readLinkOpenOutcome(page: Page, linkUrl: string): Promise<LinkOpenOutcome> {
if ((await readLocalBrowserViewUrls(page)).some((url) => url.startsWith(linkUrl))) {
return 'opened on this machine'
}
const notice = page.getByTestId('remote-browser-stream-error')
const text = (await notice.count()) > 0 ? ((await notice.first().textContent()) ?? '') : ''
return text.includes('Unable to open URL.') ? 'refused' : 'pending'
}
async function openLinkFromRemotePaneContextMenu(page: Page): Promise<void> {
const remoteFrame = page.locator('[data-testid="remote-browser-frame"]:visible').first()
await expect(remoteFrame).toBeVisible({ timeout: 60_000 })
await remoteFrame.click({ button: 'right', position: { x: 60, y: 60 }, force: true })
await expect(page.getByTestId('remote-browser-context-menu')).toBeVisible({ timeout: 30_000 })
// The item only renders once the remote hit-test resolves an anchor, so this wait is the
// wait for the link lookup itself.
const openInOrca = page.getByRole('menuitem', { name: 'Open Link In Orca Browser' })
await expect(openInOrca).toBeVisible({ timeout: 30_000 })
await openInOrca.click()
}
test('opens a remote pane link on the pane runtime and refuses to fall back to the client', async ({
testRepoPath
}, testInfo) => {
test.setTimeout(300_000)
const fixture = await startLinkFixtureServer()
const host: HeadlessPairedRuntimeHost = await launchHeadlessPairedRuntimeHost()
let client: PairedElectronClient | null = null
try {
await host.client.call('repo.add', { path: testRepoPath, kind: 'git' })
client = await launchPairedElectronClient(host.offer, testInfo, 'Remote browser link routing')
const page = client.page
const environmentId = client.environmentId
await expect
.poll(() => page.evaluate(() => window.__store?.getState().allWorktrees().length ?? 0), {
timeout: 60_000,
message: 'paired client never saw a host worktree'
})
.toBeGreaterThan(0)
const worktreeId = await page.evaluate(
() => window.__store?.getState().allWorktrees()[0]?.id ?? null
)
if (!worktreeId) {
throw new Error('paired client did not receive the host worktree')
}
// The workspace runs on the paired runtime, the way it does when the user picks that host.
await page.evaluate(
({ environmentId, worktreeId }) => {
window.__store?.getState().setActiveWorktree(worktreeId, `runtime:${environmentId}`)
},
{ environmentId, worktreeId }
)
// The host opens the page on itself: this is the "remote server" whose links must stay remote.
await host.client.call(
'browser.tabCreate',
{ worktree: `id:${worktreeId}`, url: fixture.paneUrl, activate: true },
{ timeoutMs: 30_000 }
)
await expect
.poll(() => findMirroredPage(page, worktreeId, fixture.paneUrl), {
timeout: 60_000,
message: 'the client never mirrored the host browser page'
})
.not.toBeNull()
const pane = await findMirroredPage(page, worktreeId, fixture.paneUrl)
if (!pane) {
throw new Error('mirrored host browser page disappeared')
}
expect(pane.handleEnvironmentId).toBe(environmentId)
await focusMirroredPage(page, worktreeId, pane.pageId)
const paneCountBeforeOpen = await page.getByTestId('remote-browser-pane').count()
// Act 1: the healthy path must land on the runtime, end to end.
await openLinkFromRemotePaneContextMenu(page)
await expect
.poll(
async () =>
(await readHostBrowserUrls(host, worktreeId)).filter((url) =>
url.startsWith(fixture.linkUrl)
).length,
{ timeout: 60_000, message: 'the link never opened as a browser tab on the host runtime' }
)
.toBe(1)
// The host's browser really fetched it; a tab record alone would not prove a load.
expect(fixture.linkLoadCount()).toBeGreaterThan(0)
// One more remote pane, and still nothing rendered by this machine's own browser.
await expect(page.getByTestId('remote-browser-pane')).toHaveCount(paneCountBeforeOpen + 1, {
timeout: 60_000
})
expect(await readRemotePaneUrls(page, worktreeId)).toContainEqual(
expect.stringContaining(fixture.linkUrl)
)
expect(await readLocalBrowserViewUrls(page)).toHaveLength(0)
// Drop every tab except the pane's, so the second act drives the pane it started with.
await page.evaluate(
({ paneUrl, worktreeId }) => {
const state = window.__store?.getState()
for (const workspace of state?.browserTabsByWorktree[worktreeId] ?? []) {
const pages = state?.browserPagesByWorkspace[workspace.id] ?? []
if (!pages.some((browserPage) => browserPage.url.startsWith(paneUrl))) {
state?.closeBrowserTab(workspace.id)
}
}
},
{ paneUrl: fixture.paneUrl, worktreeId }
)
await expect(page.getByTestId('remote-browser-pane')).toHaveCount(paneCountBeforeOpen, {
timeout: 60_000
})
// Act 2: the user moves this workspace onto their own machine while the runtime's page is
// still on screen. Opening the link must fail in the pane, not load the runtime's dev server
// here — the client has no business serving a page for a workspace it does not run.
await page.evaluate(
({ localHostId, worktreeId }) => {
window.__store?.getState().setActiveWorktree(worktreeId, localHostId)
},
{ localHostId: LOCAL_EXECUTION_HOST_ID, worktreeId }
)
await focusMirroredPage(page, worktreeId, pane.pageId)
const hostUrlsBefore = await readHostBrowserUrls(host, worktreeId)
const linkLoadsBefore = fixture.linkLoadCount()
await openLinkFromRemotePaneContextMenu(page)
// Wait for the click to produce an outcome — refusal or a local page — so the assertion below
// reports which one happened instead of racing past a fallback that lands a moment later.
await expect
.poll(() => readLinkOpenOutcome(page, fixture.linkUrl), {
timeout: 30_000,
message: 'the link open produced neither a refusal nor a page'
})
.not.toBe('pending')
expect(await readLinkOpenOutcome(page, fixture.linkUrl)).toBe('refused')
// The workspace must still be the local one, or the refusal above proved nothing.
expect(
await page.evaluate(() => window.__store?.getState().activeWorkspaceExecutionHostId ?? null)
).toBe(LOCAL_EXECUTION_HOST_ID)
// Nothing rendered here, nothing new on the host, and nobody fetched the link anywhere.
expect(await readLocalBrowserViewUrls(page)).toHaveLength(0)
expect(await readHostBrowserUrls(host, worktreeId)).toEqual(hostUrlsBefore)
expect(fixture.linkLoadCount()).toBe(linkLoadsBefore)
} finally {
if (client) {
await client.dispose()
}
await host.dispose()
await fixture.close()
}
})