diff --git a/config/scripts/ci-e2e-job-selection.test.mjs b/config/scripts/ci-e2e-job-selection.test.mjs index 2ecf9ea5b3c..7d522b951d9 100644 --- a/config/scripts/ci-e2e-job-selection.test.mjs +++ b/config/scripts/ci-e2e-job-selection.test.mjs @@ -42,6 +42,14 @@ it.each([ expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-routing.spec.ts') }) +it.each([ + 'src/renderer/src/lib/workspace-browser-tab-open.ts', + 'src/renderer/src/lib/http-link-destinations.ts', + 'src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.ts' +])('routes the managed terminal link oracle from %s', (path) => { + expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-terminal-browser-link.spec.ts') +}) + const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8')) const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8')) const classify = (specs) => classifyE2eJobs(JSON.stringify(specs)) diff --git a/config/scripts/pr-e2e-source-routing.mjs b/config/scripts/pr-e2e-source-routing.mjs index d580c176a3a..ba15230049d 100644 --- a/config/scripts/pr-e2e-source-routing.mjs +++ b/config/scripts/pr-e2e-source-routing.mjs @@ -170,6 +170,15 @@ export const PR_E2E_SOURCE_ROUTES = [ file ) }, + { + id: 'ssh.orcad-terminal-browser-link', + specs: ['tests/e2e/ssh-orcad-terminal-browser-link.spec.ts'], + matches: (file) => + isProductSource(file) && + /^src\/renderer\/src\/(?:lib\/(?:workspace-browser-tab-open|http-link-(?:destinations|routing)|ssh-workspace-browser-route-eligibility)|components\/terminal-pane\/terminal-http-link-source-owner)\.ts$/.test( + file + ) + }, { id: 'ssh.orcad-browser-drop-owner', specs: ['tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'], diff --git a/src/renderer/src/lib/workspace-browser-tab-open.test.ts b/src/renderer/src/lib/workspace-browser-tab-open.test.ts index a85a37d7b99..f6cc86f0d30 100644 --- a/src/renderer/src/lib/workspace-browser-tab-open.test.ts +++ b/src/renderer/src/lib/workspace-browser-tab-open.test.ts @@ -599,3 +599,116 @@ describe('openWorkspaceBrowserTab', () => { expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() }) }) + +describe('links from a managed server reached over SSH', () => { + function sshDeployedRuntimeState( + orcadDeployment: Record, + settings: Record = {} + ): Record { + return { + ...ownerState(toRuntimeExecutionHostId('orcad-env')), + runtimeEnvironments: [{ id: 'orcad-env', orcadDeployment }], + // Headless orcad advertises no screencast backend. + runtimeStatusByEnvironmentId: new Map([ + ['orcad-env', { status: { capabilities: [] }, checkedAt: 1 }] + ]), + settings, + createBrowserTab: vi.fn(), + defaultBrowserSessionProfileId: 'client-profile', + defaultBrowserSessionProfileIdByHostId: {} + } + } + const sshDeployment = { + kind: 'ssh', + sshTargetId: 'ssh-target', + sshTargetGeneration: 2, + localPort: 41000, + remotePort: 41001 + } + + it('opens the link in an SSH-routed desktop tab the runtime workspace still owns', async () => { + mocks.state = sshDeployedRuntimeState(sshDeployment) + + expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( + true + ) + await openWorkspaceBrowserTab({ + workspaceId: WORKSPACE_ID, + url: 'http://localhost:3000/', + intent: { kind: 'url' }, + expectedRuntimeEnvironmentId: 'orcad-env' + }) + + expect(mocks.state.createBrowserTab).toHaveBeenCalledWith( + WORKSPACE_ID, + 'http://localhost:3000/', + expect.objectContaining({ + browserRuntimeEnvironmentId: null, + executionHostId: toRuntimeExecutionHostId('orcad-env') + }) + ) + expect(mocks.createRemote).not.toHaveBeenCalled() + }) + + it('keeps a screencast-capable deployment on its runtime browser', async () => { + mocks.state = { + ...sshDeployedRuntimeState(sshDeployment), + ...browserCapableRuntime('orcad-env') + } + + await openWorkspaceBrowserTab({ + workspaceId: WORKSPACE_ID, + url: 'http://localhost:3000/', + intent: { kind: 'url' }, + expectedRuntimeEnvironmentId: 'orcad-env' + }) + + expect(mocks.createRemote).toHaveBeenCalledWith( + expect.objectContaining({ environmentId: 'orcad-env', waitForRegistration: true }) + ) + expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() + }) + + it.each([ + ['SSH browser routing is off', sshDeployment, { browserSshWorkspaceRoutingEnabled: false }], + [ + 'the user browses this host from this device', + sshDeployment, + { browserSshWorkspaceRoutingDisabledTargetIds: ['ssh-target'] } + ], + ['the server is a WSL deployment', { kind: 'wsl', distro: 'Ubuntu' }, {}] + ])('offers only the system browser when %s', async (_label, deployment, settings) => { + mocks.state = sshDeployedRuntimeState(deployment, settings) + + expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( + false + ) + await expect( + openWorkspaceBrowserTab({ + workspaceId: WORKSPACE_ID, + url: 'http://localhost:3000/', + intent: { kind: 'url' }, + expectedRuntimeEnvironmentId: 'orcad-env' + }) + ).rejects.toThrow('Unable to open URL.') + expect(mocks.state.createBrowserTab).not.toHaveBeenCalled() + expect(mocks.createRemote).not.toHaveBeenCalled() + }) + + it('does not lend the SSH route to a link asserting another runtime', () => { + mocks.state = sshDeployedRuntimeState(sshDeployment) + + expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'other-env')).toBe( + false + ) + }) + + it('offers no in-app browser from the paired web client', () => { + mocks.pairedWeb = true + mocks.state = sshDeployedRuntimeState(sshDeployment) + + expect(canOpenWorkspaceBrowserTabOnRuntime(mocks.getState(), WORKSPACE_ID, 'orcad-env')).toBe( + false + ) + }) +}) diff --git a/src/renderer/src/lib/workspace-browser-tab-open.ts b/src/renderer/src/lib/workspace-browser-tab-open.ts index cb7352e3dc5..e95eecbc288 100644 --- a/src/renderer/src/lib/workspace-browser-tab-open.ts +++ b/src/renderer/src/lib/workspace-browser-tab-open.ts @@ -1,4 +1,3 @@ -import { translate } from '@/i18n/i18n' import { createWebRuntimeSessionBrowserTab } from '@/runtime/web-runtime-browser-creation' import { useAppStore } from '@/store' import type { AppState } from '@/store/types' @@ -7,15 +6,20 @@ import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host' -import { SEARCH_ENGINE_LABELS, type SearchEngine } from '../../../shared/browser-url' -import type { BrowserClientHostPlacementPreference } from '../../../shared/browser-client-host-placement' +import type { SearchEngine } from '../../../shared/browser-url' import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants' import { BROWSER_SCREENCAST_RUNTIME_CAPABILITY } from '../../../shared/protocol-version' +import type { BrowserClientHostPlacementPreference } from '../../../shared/browser-client-host-placement' import { getClientCreationActionPolicy, type ClientCreationActionAvailability } from './client-creation-action-policy' import { resolveWorktreeOperationRoute } from './worktree-operation-route' +import { + intentPresentation, + openFailure, + validateTarget +} from './workspace-browser-tab-presentation' import { getExecutionHostIdForWorktree } from './worktree-runtime-owner' import { resolveSshWorkspaceBrowserRouteEligibility } from './ssh-workspace-browser-route-eligibility' @@ -65,6 +69,39 @@ function isExpectedRuntimeBrowserRoute( ) } +// Why: a managed server reached over SSH has no browser of its own, so its links open on this +// desktop, whose page the SSH-routed browser gate sends through that host (as new tabs do). +function isSshDeploymentClientBrowserRoute( + state: AppState, + availability: ClientCreationActionAvailability, + route: ReturnType, + workspaceId: string, + expectedRuntimeEnvironmentId: string +): boolean { + if ( + availability.state !== 'enabled' || + availability.provider !== 'local-client' || + workspaceId === FLOATING_TERMINAL_WORKTREE_ID || + !route + ) { + return false + } + const expectedEnvironmentId = expectedRuntimeEnvironmentId.trim() + const host = parseExecutionHostId(route.executionHostId) + const eligibility = resolveSshWorkspaceBrowserRouteEligibility( + route.executionHostId, + state.settings, + state.runtimeEnvironments + ) + return ( + Boolean(expectedEnvironmentId) && + route.runtimeEnvironmentId?.trim() === expectedEnvironmentId && + host?.kind === 'runtime' && + host.environmentId === expectedEnvironmentId && + eligibility?.eligible === true + ) +} + export function canOpenWorkspaceBrowserTabOnRuntime( state: AppState, workspaceId: string, @@ -72,12 +109,21 @@ export function canOpenWorkspaceBrowserTabOnRuntime( ): boolean { const availability = getClientCreationActionPolicy(state, workspaceId)['managed-browser'] const route = resolveWorktreeOperationRoute(state, workspaceId) - return isExpectedRuntimeBrowserRoute( - state, - availability, - route, - workspaceId, - expectedRuntimeEnvironmentId + return ( + isExpectedRuntimeBrowserRoute( + state, + availability, + route, + workspaceId, + expectedRuntimeEnvironmentId + ) || + isSshDeploymentClientBrowserRoute( + state, + availability, + route, + workspaceId, + expectedRuntimeEnvironmentId + ) ) } @@ -117,82 +163,21 @@ export function canOpenWorkspaceBrowserTabOnSsh( return isExpectedSshBrowserRoute(state, availability, route, workspaceId, expectedSshConnectionId) } -// Why: concurrent URL tabs are indistinguishable under a shared "Open URL" -// label until the page title loads; the query string stays out so a typed URL -// does not park credentials or tokens in persisted tab state. -function urlTabTitle(url: string): string | null { - try { - // host, not hostname: localhost:3000 and localhost:5173 are different tabs. - const parsed = new URL(url) - return `${parsed.host}${parsed.pathname === '/' ? '' : parsed.pathname}` - } catch { - return null - } -} - -function intentPresentation( - intent: WorkspaceBrowserTabIntent, - url: string -): { - error: string - title: string -} { - if (intent.kind === 'url') { - return { - error: translate('auto.lib.workspace.browser.tab.open.urlFailed', 'Unable to open URL.'), - title: - urlTabTitle(url) ?? - translate('auto.components.tab.bar.TabBarCreateEntry.7cdf8ee0c8', 'Open URL') - } - } - const engine = SEARCH_ENGINE_LABELS[intent.engine] - return { - error: translate( - 'auto.lib.workspace.browser.tab.open.searchFailed', - 'Unable to search with {{value0}}.', - { value0: engine } - ), - title: translate( - 'auto.components.tab.bar.TabBarCreateEntry.searchProvider', - 'Search {{value0}}', - { value0: engine } - ) - } -} - -// Why: the UI string stays friendly and query-free, but the diagnosable reason -// rides along as `cause` so a failed open is not indistinguishable in logs. -function openFailure(message: string, reason: string, cause?: unknown): Error { - // Why: callers only surface `message`, so log the reason here or a failed open - // leaves no trace at all. The reason alone — never `cause` — keeps the typed - // URL and search query out of the console. - console.warn(`[workspace-browser-tab-open] ${reason}`) - return new Error(message, { - cause: new Error(reason, cause === undefined ? undefined : { cause }) - }) -} - -function validateTarget(url: string): boolean { - try { - const parsed = new URL(url) - return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && !!parsed.hostname - } catch { - return false - } -} - function createClientBrowserTab( state: AppState, request: OpenWorkspaceBrowserTabRequest, hostId: ExecutionHostId, - presentation: { error: string; title: string } + presentation: { error: string; title: string }, + // Why: a desktop-hosted page in a runtime workspace stays owned by that runtime, or the + // unified tab is disowned from its worktree (palette and activation skip it). + ownerHostId: ExecutionHostId = hostId ): void { try { state.createBrowserTab(request.workspaceId, request.url, { activate: request.focusOnCreate !== false, browserRuntimeEnvironmentId: null, focusAddressBar: false, - executionHostId: hostId, + executionHostId: ownerHostId, sessionProfileId: state.defaultBrowserSessionProfileIdByHostId[hostId] ?? state.defaultBrowserSessionProfileId, @@ -238,16 +223,26 @@ export async function openWorkspaceBrowserTab( if (expectedEnvironmentId !== null && expectedSshConnectionId !== null) { throw openFailure(presentation.error, 'browser owner assertion is ambiguous') } - if ( + const runtimeOwnerRoute = expectedEnvironmentId !== null && - !isExpectedRuntimeBrowserRoute( + isExpectedRuntimeBrowserRoute( state, availability, route, request.workspaceId, expectedEnvironmentId ) - ) { + const sshDeploymentClientRoute = + expectedEnvironmentId !== null && + !runtimeOwnerRoute && + isSshDeploymentClientBrowserRoute( + state, + availability, + route, + request.workspaceId, + expectedEnvironmentId + ) + if (expectedEnvironmentId !== null && !runtimeOwnerRoute && !sshDeploymentClientRoute) { throw openFailure(presentation.error, 'asserted runtime cannot provide this managed browser') } if ( @@ -280,10 +275,16 @@ export async function openWorkspaceBrowserTab( ) } // An asserted runtime owns links opened from remote panes; provider policy may describe the - // viewing client's generic browser surface rather than that pane's execution host. - if (expectedEnvironmentId === null && availability.provider === 'local-client') { + // viewing client's generic browser surface rather than that pane's execution host. The one + // exception is an SSH-deployed server, whose links this desktop hosts on the SSH route. + if ( + (expectedEnvironmentId === null || sshDeploymentClientRoute) && + availability.provider === 'local-client' + ) { const localHostId = host && host.kind !== 'runtime' ? host.id : LOCAL_EXECUTION_HOST_ID - createClientBrowserTab(state, request, localHostId, presentation) + const ownerHostId = + host?.kind === 'runtime' && route.executionHostId ? route.executionHostId : localHostId + createClientBrowserTab(state, request, localHostId, presentation, ownerHostId) return } let created = false diff --git a/src/renderer/src/lib/workspace-browser-tab-presentation.ts b/src/renderer/src/lib/workspace-browser-tab-presentation.ts new file mode 100644 index 00000000000..c6e8996b5dd --- /dev/null +++ b/src/renderer/src/lib/workspace-browser-tab-presentation.ts @@ -0,0 +1,67 @@ +import { translate } from '@/i18n/i18n' +import { SEARCH_ENGINE_LABELS } from '../../../shared/browser-url' +import type { WorkspaceBrowserTabIntent } from './workspace-browser-tab-open' + +// Why: concurrent URL tabs are indistinguishable under a shared "Open URL" +// label until the page title loads; the query string stays out so a typed URL +// does not park credentials or tokens in persisted tab state. +function urlTabTitle(url: string): string | null { + try { + // host, not hostname: localhost:3000 and localhost:5173 are different tabs. + const parsed = new URL(url) + return `${parsed.host}${parsed.pathname === '/' ? '' : parsed.pathname}` + } catch { + return null + } +} + +export function intentPresentation( + intent: WorkspaceBrowserTabIntent, + url: string +): { + error: string + title: string +} { + if (intent.kind === 'url') { + return { + error: translate('auto.lib.workspace.browser.tab.open.urlFailed', 'Unable to open URL.'), + title: + urlTabTitle(url) ?? + translate('auto.components.tab.bar.TabBarCreateEntry.7cdf8ee0c8', 'Open URL') + } + } + const engine = SEARCH_ENGINE_LABELS[intent.engine] + return { + error: translate( + 'auto.lib.workspace.browser.tab.open.searchFailed', + 'Unable to search with {{value0}}.', + { value0: engine } + ), + title: translate( + 'auto.components.tab.bar.TabBarCreateEntry.searchProvider', + 'Search {{value0}}', + { value0: engine } + ) + } +} + +// Why: the UI string stays friendly and query-free, but the diagnosable reason +// rides along as `cause` so a failed open is not indistinguishable in logs. +export function openFailure(message: string, reason: string, cause?: unknown): Error { + // Why: callers only surface `message`, so log the reason here or a failed open + // leaves no trace at all. The reason alone — never `cause` — keeps the typed + // URL and search query out of the console. + console.warn(`[workspace-browser-tab-open] ${reason}`) + return new Error(message, { + cause: new Error(reason, cause === undefined ? undefined : { cause }) + }) +} + +export function validateTarget(url: string): boolean { + try { + const parsed = new URL(url) + return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && !!parsed.hostname + } catch { + return false + } +} diff --git a/tests/e2e/ssh-orcad-terminal-browser-link.spec.ts b/tests/e2e/ssh-orcad-terminal-browser-link.spec.ts new file mode 100644 index 00000000000..69c77ce9a68 --- /dev/null +++ b/tests/e2e/ssh-orcad-terminal-browser-link.spec.ts @@ -0,0 +1,200 @@ +import type { ElectronApplication, Page } from '@stablyai/playwright-test' +import { expect, test } from './helpers/orca-app' +import { createRestartSession } from './helpers/orca-restart' +import { waitForSessionReady } from './helpers/store' +import { + cleanupDockerSshHostTarget, + DOCKER_SSH_HOST_REMOTE_REPO_PATH, + startDockerSshHostTarget +} from './helpers/docker-ssh-host-target' +import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile' +import { + readSshRemoteOnlyRequests, + SSH_REMOTE_ONLY_ORIGIN, + SSH_REMOTE_ONLY_PORT, + startSshRemoteOnlyBrowserFixture +} from './helpers/ssh-remote-only-browser-fixture' +import { convertAndRetain } from './helpers/orcad-convert-flow' +import { + activateManagedWorkspace, + expectPaneText, + managedEnvironmentId, + openTerminalTab, + ORCAD_TEMPLATE, + skipUnlessManagedDockerHost, + typeInTerminal +} from './helpers/orcad-managed-workspace' + +skipUnlessManagedDockerHost() + +async function terminalLinkPoint( + page: Page, + tabId: string, + link: string +): Promise<{ x: number; y: number }> { + return page.evaluate( + ({ tabId, link }) => { + const pane = window.__paneManagers?.get(tabId)?.getActivePane?.() + const screen = pane?.terminal.element?.querySelector('.xterm-screen') + const cell = pane?.terminal.dimensions?.css.cell + if (!pane || !screen || !cell?.width || !cell.height) { + throw new Error('terminal screen unavailable') + } + const buffer = pane.terminal.buffer.active + for (let row = pane.terminal.rows - 1; row >= 0; row -= 1) { + const text = buffer.getLine(buffer.viewportY + row)?.translateToString(false) ?? '' + const column = text.indexOf(link) + if (column !== -1) { + const rect = screen.getBoundingClientRect() + return { + x: rect.left + (column + link.length / 2) * cell.width, + y: rect.top + (row + 0.5) * cell.height + } + } + } + throw new Error(`terminal link ${link} not on screen`) + }, + { tabId, link } + ) +} + +/** Prints origin+path as one link; split apart so the typed command never shows the whole URL. */ +async function printLink(page: Page, tabId: string, origin: string, path: string): Promise { + await typeInTerminal(page, tabId, `printf '%s%s\\n' '${origin}' '${path}'`) + await expectPaneText(page, tabId, `${origin}${path}`) +} + +function browserTabUrls(page: Page, worktreeId: string): Promise { + return page.evaluate( + (id) => (window.__store?.getState().browserTabsByWorktree[id] ?? []).map((tab) => tab.url), + worktreeId + ) +} + +function browserTabIdForUrl(page: Page, worktreeId: string, url: string): Promise { + return page.evaluate( + ({ id, url }) => + (window.__store?.getState().browserTabsByWorktree[id] ?? []).find((tab) => tab.url === url) + ?.id ?? null, + { id: worktreeId, url } + ) +} + +/** The opened page must be the visible browser surface and render the host's response. */ +async function expectRenderedLink(page: Page, worktreeId: string, url: string): Promise { + await expect.poll(() => browserTabIdForUrl(page, worktreeId, url)).not.toBeNull() + const tabId = await browserTabIdForUrl(page, worktreeId, url) + const webview = page.locator(`[data-browser-overlay-tab-id="${tabId}"] webview`) + await expect(webview).toBeVisible({ timeout: 30_000 }) + await expect + .poll( + () => + webview.evaluate(async (guest: Electron.WebviewTag) => { + try { + return await guest.executeJavaScript('document.querySelector("#marker")?.textContent') + } catch { + return null + } + }), + { timeout: 60_000, message: 'the opened page never rendered the host response' } + ) + .toMatch(/^cookie:/) +} + +test('a managed SSH terminal link opens in the Orca Browser through the SSH host', async (// oxlint-disable-next-line no-empty-pattern -- Owns app launch. +{}, testInfo) => { + test.setTimeout(5 * 60_000) + const target = startDockerSshHostTarget(testInfo) + const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: ORCAD_TEMPLATE! }) + let app: ElectronApplication | null = null + try { + startSshRemoteOnlyBrowserFixture(target) + // The first launch only creates the profile the SSH host is saved into. + const first = await session.launch() + app = first.app + await waitForSessionReady(first.page) + await session.close(first.app) + app = null + const seeded = seedRelayEraProfile( + session.userDataDir, + { + label: `orcad terminal link E2E ${Date.now()}`, + host: target.host, + port: target.port, + username: 'root', + identityFile: target.identityFile, + identitiesOnly: true, + relayGracePeriodSeconds: 1 + }, + { repoPath: DOCKER_SSH_HOST_REMOTE_REPO_PATH, folderPath: '/tmp' } + ) + const launched = await session.launch() + app = launched.app + const page = launched.page + await waitForSessionReady(page) + await convertAndRetain(page, session.userDataDir, seeded) + const environmentId = await managedEnvironmentId(page, seeded.targetId) + await activateManagedWorkspace(page, seeded.worktreeId, environmentId) + await page.evaluate(() => + window.__store?.getState().updateSettings({ + openLinksInApp: true, + terminalLinkClickBehavior: 'actions' + }) + ) + const tabId = await openTerminalTab(page, seeded.worktreeId) + + // The link action popover offers the Orca Browser for a managed server's pane. + const popoverUrl = `${SSH_REMOTE_ONLY_ORIGIN}/echo/popover` + await printLink(page, tabId, SSH_REMOTE_ONLY_ORIGIN, '/echo/popover') + const popoverPoint = await terminalLinkPoint(page, tabId, popoverUrl) + await page.mouse.move(popoverPoint.x, popoverPoint.y) + await page.mouse.click(popoverPoint.x, popoverPoint.y) + const popover = page.locator('[data-terminal-link-action-popover]') + await expect(popover).toBeVisible() + await expect(popover.getByRole('button').filter({ hasText: 'System Browser' })).toBeVisible() + const orcaBrowser = popover.getByRole('button').filter({ hasText: 'Orca Browser' }) + await expect(orcaBrowser).toBeVisible() + await page.screenshot({ path: testInfo.outputPath('managed-terminal-link-popover.png') }) + await orcaBrowser.click() + await expect + .poll(() => browserTabUrls(page, seeded.worktreeId), { timeout: 30_000 }) + .toContain(popoverUrl) + // Only the SSH host resolves this origin, so the request proves the page egressed there. + await expect + .poll(() => readSshRemoteOnlyRequests(target).map((request) => request.path), { + timeout: 60_000, + message: 'the Orca Browser page never reached the SSH host' + }) + .toContain('/echo/popover') + await expectRenderedLink(page, seeded.worktreeId, popoverUrl) + + // Cmd/Ctrl-click opens a remote localhost dev-server URL straight into the Orca Browser. + const modifierUrl = `http://localhost:${SSH_REMOTE_ONLY_PORT}/echo/modifier` + await printLink(page, tabId, `http://localhost:${SSH_REMOTE_ONLY_PORT}`, '/echo/modifier') + const modifierPoint = await terminalLinkPoint(page, tabId, modifierUrl) + await page.mouse.move(modifierPoint.x, modifierPoint.y) + await page.keyboard.down('ControlOrMeta') + await page.mouse.click(modifierPoint.x, modifierPoint.y) + await page.keyboard.up('ControlOrMeta') + await expect + .poll(() => browserTabUrls(page, seeded.worktreeId), { timeout: 30_000 }) + .toContain(modifierUrl) + await expect + .poll(() => readSshRemoteOnlyRequests(target).map((request) => request.path), { + timeout: 60_000, + message: 'the Cmd/Ctrl-clicked localhost link never reached the SSH host' + }) + .toContain('/echo/modifier') + await expectRenderedLink(page, seeded.worktreeId, modifierUrl) + await page.screenshot({ path: testInfo.outputPath('managed-terminal-link-opened.png') }) + } finally { + try { + if (app) { + await session.close(app) + } + await session.dispose() + } finally { + cleanupDockerSshHostTarget(target) + } + } +})