Files
orca/config/scripts/pr-e2e-source-routing.mjs
T
Neil f59bd43c46 fix(wsl-host): Remove keeps a WSL host's server that another Orca on this computer shares (R39) (#34699)
* fix(wsl-host): Remove keeps a WSL host's server that another Orca on this computer shares

A WSL host's server lives in the Linux login's ~/.orca, so two Orca installs on
one Windows account (stable and dev), or a desktop reaching the distro over SSH,
share one server. Remove used to close every terminal there and decommission
it, ending the other desktop's sessions and agents.

The server now answers host.peerDesktops (capability orcad.peer-desktops.v1):
the other desktops paired with it, whether each is connected now, and when each
was last seen. Before tearing anything down, Remove asks. If another desktop is
connected or was seen in the last 30 days, it only closes this desktop's bridge
and reports the server kept. If the server can't answer, the teardown is
refused. An older server without the capability gets today's teardown, plus a
warning.

* fix(wsl-host): a shared Remove releases this Orca from the host, and says the host keeps running

- On a shared server, Remove now revokes this desktop's own device (and its
  phones) on the host, through the new host.releaseDesktop, before letting go.
  Otherwise its recent row would make a peer's later Remove keep the server
  forever. Peers also skip rows of the caller's own installation (same caller
  fingerprint), such as an enrollment from before a re-add.
- The renderer says when a removal kept the host for another Orca, and the
  Remove confirmation no longer promises to end terminals unconditionally.
- Remove-path files now route CI to the fake transport e2e spec, which
  exercises Remove.

* i18n(wsl-host): Russian and Traditional Chinese for the shared-host Remove copy

* fix(wsl-host): tell desktops apart by device key, and release this Orca on every Remove

- Peers are no longer hidden by caller fingerprint. Every device-key fork of
  one pairing token row inherits that row's fingerprint, so two installations
  that paired through one code hid each other, and one's Remove could tear the
  server down under the other. Only a shared device public key now marks the
  same installation, as with a re-add under a new token.
- This desktop's own device is released as the last call over the bridge on
  every Remove that proceeds (no terminals left), not only a shared one. A
  later Add then Remove never meets a stale row of ours.
- When the release fails on a shared Remove, the toast says the host may keep
  running for a while. The Russian confirmation copy is corrected.

* fix(wsl-host): move undo refuses a removed host whose server another Orca kept

A Remove that kept a WSL host's server for another Orca (R39) now records
that. Before, undo of a move onto that host read the missing environment as
"its terminals and automations stopped with it", and would have restored the
project here while the host's copies kept running, duplicating its agents and
automations. Undo now reads such a host as unreachable and refuses, as it does
for any host it can't reach. A later Remove that proves the server stopped
clears the record. If the record can't be saved, the host stays listed as
pending instead.

* i18n(wsl-host): Turkish for the shared-host Remove copy

* fix(wsl-host): each WSL desktop splits off its own row from orcad's startup pairing

orcad publishes one startup pairing offer and never rotates it, so every
desktop that enrolled through it shared one token row and device id. Stable and
dev Orca on one distro were invisible to each other as peers, and releasing that
shared row broke the next re-add and pairing repair.

- New host.forkDesktop (same orcad.peer-desktops.v1 capability). A runtime
  device on a shared desktop token mints its own token row, forked from it and
  named by the caller. Forks share the device-key fork cap, and a released fork
  frees its slot.
- The WSL desktop forks right after enrolling through the startup offer, or
  after a repair lands back on it, and keeps the forked pairing (named after its
  userData folder, e.g. Orca or Orca Dev). On an older orcad, or if the fork
  fails, it keeps the shared pairing and warns.
- A token row that desktops forked from counts as a peer only while something
  is connected on it. One that nobody forked from keeps the last-seen rule.

* fix(wsl-host): keep host.forkDesktop to the startup offer's holder, over E2EE, with a clean label

host.forkDesktop mints a credential, so:
- only the holder of the desktop pairing offer this server advertised last
  (orcad's startup offer) may fork; an older offer, a fork, a phone or any
  other row is refused;
- the fork takes exactly the caller's grants and reach; params are just a label
  (strict, so a grant request is refused);
- it runs only on an authenticated E2EE socket; the token goes back only in
  that reply and is never logged;
- the label is display-only: control, zero-width and bidi characters stripped,
  whitespace collapsed, at most 64 characters; an empty result is refused.

* refactor(wsl-host): count only Orcas connected right now as WSL host peers; drop fork and release

Review found the token fork and self-release add more identity state than R39
warrants: orphan forks, a desktop left on the shared token releasing it, and a
changed caller fingerprint. So this drops host.forkDesktop, host.releaseDesktop
and the 30-day last-seen rule, and restores the restart test.

A peer is now another Orca installation with a control socket open on the
server right now. The desktop declares its installation (a hash of its userData
path) with host.declareClient on its shared-control socket after every ready,
including reconnects. The server tags that socket and drops the tag when it
closes. host.peerDesktops takes the caller's installation id, so its own other
sockets (multiplex, one-shot) never count, and two desktops sharing one startup
token stay distinct. Undeclared sockets, phones and browser sessions are
ignored. Peer names go through sanitizeDeviceLabel, and a startup-offer
"CLI <date>" name shows as "another Orca". An older orcad lacks the method:
nothing is declared, and Remove keeps today's teardown with a warning.

Limit: an Orca that's closed while its terminals keep running isn't protected.

* fix(wsl-host): keep a reconnecting Orca as a WSL host peer for 60 seconds

Lost contact isn't proof an Orca left. When an installation's last declared
control socket closes (a bridge exit, an orcad restart, resume from sleep,
reconnect backoff), the server keeps counting it as a peer for 60 seconds, or
until it re-declares. A Remove inside that gap no longer tears down its
terminals.

* fix(wsl-host): say an open Orca keeps the host, and name each peer once

- The Remove confirmation now reads "Unless another Orca that's open on this
  computer is using it", since only a connected Orca keeps the server.
  Updated in all 10 locales.
- The kept-host toast dedupes peer names, so several unnamed peers read
  "another Orca" once.

* test(wsl-host): pin the reconnect grace to the last socket's close, and to the server's own forget

- The unit test advances the clock between the two closes and checks that the
  peer still counts when the first close's grace would have run out.
- The integration test waits until the server has dropped the closed socket
  before reading peers, so it fails without the grace (checked by setting the
  grace to zero).

* fix(wsl-host): sweep expired reconnect graces on every close, on a monotonic clock

Expired graces are dropped at the start of forget() as well as peers(), so the
map stays bounded even when nobody asks for peers. The default clock is
performance.now(), so a wall-clock jump can't stretch or cut the grace.

* i18n(wsl-host): German for the shared-host Remove copy
2026-10-28 04:27:30 -07:00

638 lines
33 KiB
JavaScript

import process from 'node:process'
import { pathToFileURL } from 'node:url'
import {
DAEMON_INIT_TEST_SUPPORT,
isDaemonInitTestSupportChange,
isUnitTestSupportSource
} from './pr-code-change-scope.mjs'
import { MANAGED_SSH_E2E_SPEC_PREFIX } from './ci-e2e-job-selection.mjs'
import { LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs'
import { PR_BROWSER_E2E_SOURCE_ROUTES } from './pr-browser-e2e-source-routes.mjs'
const isProductSource = (file) => !/\.test\.tsx?$/.test(file) && !isUnitTestSupportSource(file)
// The core xterm patch owns the helper textarea an input method attaches to.
const XTERM_CORE_PATCH =
/^config\/patches\/(?:@xterm__xterm@[^/]+\.patch|xterm-src\/@xterm__xterm@[^/]+\.src\.patch)$/
const NATIVE_IME_PRODUCT_SOURCE =
/^(?:src\/shared\/terminal-unicode-provider\.ts$|src\/renderer\/src\/lib\/pane-manager\/terminal-ime-|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|terminal-ios-hangul-|xterm-bypass-policy))/
/** The harness itself: the session runner, the boundary probes, and the native specs. */
const NATIVE_IME_HARNESS =
/^(?:config\/scripts\/focus-nested-wayland-terminal\.sh$|config\/scripts\/(?:run-terminal-ibus-hangul-e2e|terminal-ime-engagement-receipt)\.mjs$|tests\/e2e\/terminal-ime-(?:boundary-probe|byte-reader|engagement-receipt)\.ts$|tests\/e2e\/terminal-(?:ibus-hangul|hangul-terminating-digit|macos-2set-korean)-native\.spec\.ts$)/
// This spec requires ORCA_SIDEBAR_INPUTS=main; ordinary PR builds can only skip it.
export const MANUAL_ONLY_E2E_SPECS = ['tests/e2e/ssh-orcad-phone-feed-list.spec.ts']
// Shared fixtures and workflow changes must exercise every managed-SSH consumer.
const MANAGED_SSH_HARNESS =
/^tests\/e2e\/helpers\/(?:orcad-managed-workspace|orcad-convert-(?:flow|host|teardown)|orcad-template-variant|orcad-upgrade-(?:profile|retained-state)|docker-ssh-host-target|reserve-local-port|ssh-remote-only-browser-fixture|windows-host-orcad-processes)\.ts$/
// An enabled SSH automation moves with its host and is re-enabled on the server (#29083).
const ENABLED_AUTOMATION_MOVE =
/^src\/(?:main\/(?:persistence\/(?:migrating-orcad-catalog\/orcad-(?:source-automation-state|source-dormant-state|source-dependency-census|destination-automation-state)|scheduling-automations\/automation-schedule-operations)|automations\/(?:run-target-resolution|automation-dispatch-request|orcad-fenced-automation)|ssh\/orcad-migration-(?:moved-automations|source-retention))|shared\/orcad-migration\/orcad-migration-(?:dormant-automation-validation|source-cutover))\.ts$/
export const PR_E2E_SOURCE_ROUTES = [
...PR_BROWSER_E2E_SOURCE_ROUTES,
{
id: 'ssh.orcad-editor-ownership',
specs: ['tests/e2e/ssh-orcad-editor-ownership.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/runtime\/(?:web-session-existing-tab-index|web-session-tabs-sync\/(?:mirrored-editor-file-identity|tab-builders|apply-preparation-browser|state-equality-files|terminal-surfaces))\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-markdown-conversion',
specs: ['tests/e2e/ssh-orcad-markdown-conversion.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/components\/editor\/(?:useMarkdownDocuments|restored-editor-workspace-runtime-owner|migrate-restored-editor-file-owner)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-markdown-link-refresh',
specs: ['tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/components\/editor\/(?:rich-markdown-doc-link|useRichMarkdownProgrammaticSync)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-markdown-live-documents',
specs: ['tests/e2e/ssh-orcad-markdown-live-documents.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/components\/editor\/(?:useMarkdownDocuments|use-markdown-document-watch-refresh|markdown-document-list-request)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-explorer-watch-recovery',
specs: ['tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts'],
matches: (file) =>
isProductSource(file) &&
(file === 'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts' ||
file === 'src/renderer/src/hooks/worktree-file-change-event.ts')
},
{
id: 'ssh.orcad-editor-watch-recovery',
specs: ['tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/hooks\/(?:useEditorExternalWatch|editor-runtime-file-watch)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-explorer-selected-host',
specs: ['tests/e2e/ssh-orcad-explorer-selected-host.spec.ts'],
matches: (file) =>
isProductSource(file) &&
(file === 'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts' ||
file === 'src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts' ||
file === 'src/renderer/src/components/right-sidebar/FileExplorer.tsx' ||
file === 'src/renderer/src/hooks/editor-external-watch-targets.ts' ||
file === 'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts' ||
file === 'src/renderer/src/components/right-sidebar/use-file-explorer-tree-load-effects.ts')
},
{
// A paired client refuses project setup on its server's SSH host with a clear reason (#33124).
id: 'paired.ssh-host-project-setup',
specs: ['tests/e2e/headless-paired-ssh-host-project-setup.spec.ts'],
matches: (file) =>
file === 'tests/e2e/helpers/headless-paired-runtime-host.ts' ||
(isProductSource(file) &&
/^src\/(?:renderer\/src\/store\/projects\/project-host-setup-actions|main\/runtime\/runtime-project-host-setup-controller|shared\/project-host-setup-target)\.ts$/.test(
file
))
},
{
id: 'serve.orcad-mode-switch',
specs: ['tests/e2e/orcad-serve-mode-switch.spec.ts'],
matches: (file) =>
/^tests\/e2e\/helpers\/(?:orca-serve-cli-host|headless-paired-runtime-host)\.ts$/.test(
file
) ||
(isProductSource(file) &&
/^src\/(?:cli\/runtime\/(?:launch|serve-)|main\/orcad\/(?:main|orcad-entry|orcad-instance-lock|orcad-command-arguments|orcad-lifecycle)\.ts$|main\/startup\/desktop-profile-instance-lock\.ts$|main\/daemon\/daemon-(?:spawner|endpoint-adoption|init)|main\/server\/serve-)/.test(
file
))
},
{
id: 'startup.windows-missing-appdata',
specs: ['tests/e2e/windows-missing-appdata-startup.spec.ts'],
matches: (file) =>
file === 'tests/e2e/helpers/orca-serve-cli-host.ts' ||
(isProductSource(file) &&
/^src\/main\/startup\/(?:windows-app-data-path|main-process-preflight)\.ts$/.test(file))
},
{
id: 'clipboard.windows-copy-file',
specs: ['tests/e2e/file-explorer-copy-file-windows.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:main\/window\/clipboard-(?:file-copy|ipc-handlers)|renderer\/src\/components\/right-sidebar\/file-explorer-row-file-transfer)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-auto-convert',
specs: ['tests/e2e/ssh-orcad-auto-convert.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/main\/(?:ipc\/(?:ssh\/)?ssh-host-server-|(?:ssh|managed-server\/[a-z-]+)\/(?:ssh-host-server-|orcad-runtime-conversion|orcad-migration-|orcad-retained-source|orcad-runtime-deployment))/.test(
file
))
},
{
id: 'ssh.orcad-connect-failure',
specs: [
'tests/e2e/ssh-orcad-ssh-failure-reason.spec.ts',
'tests/e2e/ssh-orcad-move-anyway-inline.spec.ts',
'tests/e2e/ssh-orcad-enabled-automation-move.spec.ts'
],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
ENABLED_AUTOMATION_MOVE.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/(?:ipc\/ssh\/(?:ssh-connect-flow|ssh-host-server-)|ssh\/(?:ssh-session-start-failure|ssh-host-server-|connection\/ssh-connection-manager|orcad-migration-refusal-reason|targets\/ssh-target-orcad-preflight)|managed-server\/lifecycle\/orcad-runtime-conversion|persistence\/migrating-orcad-catalog\/orcad-source-(?:automation-state|leave-behind-view))|renderer\/src\/(?:ssh\/(?:ssh-host-server-status-copy|ssh-connect-failure-toast)|components\/settings\/(?:SshTargetServerStatus|SshHostMoveAnywayAction|SshTargetCard)))/.test(
file
))
},
{
id: 'ssh.orcad-identity-file-tokens',
specs: ['tests/e2e/ssh-orcad-identity-file-tokens.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
/^tests\/e2e\/helpers\/ssh-config-host-picker\.ts$/.test(file) ||
(isProductSource(file) &&
/^src\/main\/ssh\/(?:config\/(?:ssh-g-config-resolution|ssh-identity-file-tokens|ssh-config-path-expansion)|ssh-auth-resolution|ssh-identity-file-read)\.ts$/.test(
file
))
},
{
id: 'ssh.orcad-update-anyway',
specs: ['tests/e2e/ssh-orcad-update-anyway.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/(?:ssh\/orcad-(?:update-plan|runtime-maintenance|installed-activation|remote-deploy)|managed-server\/lifecycle\/orcad-(?:managed-update-|managed-auto-update|runtime-status))|renderer\/src\/components\/settings\/(?:managed-server-|use-managed-server-lifecycle))/.test(
file
))
},
// COMPAT(legacy-relay-bridge): revisit 2027-01-15 with src/main/ssh/legacy-relay/.
{
id: 'ssh.orcad-legacy-relay-bridge',
specs: [
'tests/e2e/ssh-orcad-legacy-relay-bridge.spec.ts',
'tests/e2e/ssh-orcad-relay-tab-only-shell.spec.ts'
],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
/^tests\/e2e\/(?:helpers\/(?:legacy-relay-docker-host|build-released-relay-bundle)|cross-version-wire\/released-relay-(?:daemon|transport))\.m?ts$/.test(
file
) ||
(isProductSource(file) &&
/(?:legacy-relay(?:-bridge|\/)|orcad-migration-terminal-gate|ssh-host-relay-terminals-on-connect|orcad-source-dependency-census)/.test(
file
))
},
{
id: 'ssh.orcad-browser-capabilities',
specs: ['tests/e2e/ssh-orcad-browser-capabilities.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/main\/(?:runtime\/(?:runtime-browser-commands-factory|orca-runtime-get-status|browser\/browser-client-page-opening)|host\/electron-browser-commands|orcad\/orcad-browser-)/.test(
file
)
},
{
id: 'browser.orcad-service-status',
specs: ['tests/e2e/ssh-orcad-browser-service-status.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/components\/browser-pane\/stream-remote\/remote-browser-stream-(?:errors|status|lifecycle|restart-attempt)\.ts$/.test(
file
)
},
{
id: 'ssh.orcad-browser-routing',
specs: ['tests/e2e/ssh-orcad-browser-routing.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:main\/browser\/local-ssh-browser|main\/ipc\/browser\.ts$|renderer\/src\/(?:lib\/(?:ssh-workspace-browser-route-eligibility|worktree-host-connection-phase)|components\/browser-pane\/use-ssh-workspace-browser-route|store\/repos\/converted-ssh-browser-pages|store\/slices\/browser\/browser-tab-actions|hooks\/ipc-events\/ssh-managed-server-state-effects))/.test(
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-desktop-browser-bridge',
specs: ['tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:shared\/desktop-browser-bridge-protocol\.ts$|main\/(?:runtime\/desktop-browser-bridge\/|browser\/desktop-browser-bridge\/|ipc\/desktop-browser-bridge-|runtime\/rpc\/methods\/desktop-browser-bridge\.ts$))/.test(
file
)
},
{
id: 'ssh.orcad-browser-drop-owner',
specs: ['tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
/^tests\/e2e\/helpers\/browser-split-(?:guest-probes|page-server)\.ts$/.test(file) ||
(isProductSource(file) &&
/^src\/renderer\/src\/(?:lib\/(?:file-preview|workspace-file-drag|worktree-runtime-owner|connection-context)\.ts|components\/(?:right-sidebar\/FileExplorer(?:Row|VirtualRows)\.tsx|browser-pane\/(?:navigate\/use-browser-page-navigation-downloads\.ts|host-guest\/attach-browser-page-webview\.ts)))$/.test(
file
))
},
{
id: 'ssh.orcad-open-in-owner',
specs: ['tests/e2e/ssh-orcad-open-in-owner.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/renderer\/src\/(?:lib\/(?:local-path-open-guard|external-editor-open-capability|worktree-runtime-owner)\.ts|components\/(?:sidebar\/(?:WorktreeOpenInMenu|WorktreeContextMenuView)\.tsx|right-sidebar\/(?:FileExplorer(?:Toolbar)?\.tsx|source-control\/listing\/entry-context-menu\.tsx)))$/.test(
file
))
},
{
id: 'ssh.orcad-terminal-root-owner',
specs: ['tests/e2e/ssh-orcad-terminal-root-owner.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
file === 'tests/e2e/helpers/terminal-workspace-root-link.ts' ||
(isProductSource(file) &&
/^src\/renderer\/src\/(?:components\/terminal-pane\/terminal-(?:worktree-path-link|file-link-actions|file-link-hit-testing|file-open-routing|link-handlers)\.ts|lib\/(?:workspace-file-host-routing|worktree-owner-route|worktree-operation-route|worktree-operation-catalog-route)\.ts)$/.test(
file
))
},
{
id: 'ssh.orcad-sleep-disconnected',
specs: ['tests/e2e/ssh-orcad-sleep-disconnected.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/runtime\/(?:workspaces\/(?:workspace-sleep-|remote-workspace-terminal-sleep)|terminal\/stop\/worktree-terminal-sleep)|renderer\/src\/components\/sidebar\/(?:sleep-|workspace-sleep-))/.test(
file
))
},
{
id: 'ssh.orcad-idle-exit',
specs: ['tests/e2e/ssh-orcad-idle-exit.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/(?:orcad\/orcad-(?:idle-|managed-idle-)|(?:ssh|managed-server\/[a-z-]+)\/orcad-(?:already-active-wake|runtime-deployment|managed-serving|managed-wake|managed-tunnel|recovery-slot|remote-launch|remote-runtime-control))|shared\/orcad-idle-exit)/.test(
file
))
},
{
// Managed SSH is the only SSH path; include Connect/Disconnect so journeys exercise reconnect (#30130).
id: 'ssh.orcad-journeys',
specs: [
'tests/e2e/ssh-orcad-terminal-journeys.spec.ts',
'tests/e2e/ssh-orcad-workspace-tools.spec.ts',
'tests/e2e/ssh-orcad-vm-provisioned-root.spec.ts'
],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
/^tests\/e2e\/helpers\/(?:paired-host-terminal|terminal-workspace-root-link)\.ts$/.test(
file
) ||
(isProductSource(file) &&
/^src\/(?:main\/(?:ssh|managed-server\/[a-z-]+)\/orcad-managed-|main\/ipc\/(?:ssh\/)?ssh-port-forward|main\/(?:ipc\/(?:(?:ssh\/)?ssh-(?:connect-flow|connection-handlers|host-user-disconnect|session-teardown)|(?:runtime-environments\/)?runtime-environment-(?:connectivity-handlers|manual-disconnect))|ssh\/(?:connection\/ssh-connection-manager|ssh-user-disconnect))\.ts$|renderer\/src\/startup\/(?:ssh-startup-reconnect|startup-ssh-connection-restore)\.ts$|renderer\/src\/runtime\/(?:web-runtime-session|web-session-close-|runtime-terminal-artifact-read\.ts$)|renderer\/src\/components\/terminal-pane\/(?:remote-runtime-|terminal-host-(?:workspace-file|file-link-existence)\.ts$))/.test(
file
))
},
{
// A Claude chat a managed host's CLI refuses to start names why (#33768).
id: 'ssh.orcad-claude-chat-start-failure',
specs: ['tests/e2e/ssh-orcad-claude-chat-start-failure.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
file === 'tests/e2e/fixtures/structured-claude-stub.cjs' ||
(isProductSource(file) &&
/^src\/(?:main\/(?:claude\/claude-(?:stream-json-connection|stream-json-exit-error|structured-session-exit-lifecycle)|native-chat\/agent-session-wire\/structured-agent-session-failure-text)|renderer\/src\/components\/native-chat\/structured-agent-session-delivery-notices)\.ts$/.test(
file
))
},
{
// A reconnect the host cannot image must still bring an idle full-screen program up to date (#26887).
id: 'ssh.orcad-reconnect-idle-frame',
specs: ['tests/e2e/ssh-orcad-terminal-reconnect-idle-frame.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/(?:renderer\/src\/components\/terminal-pane\/(?:remote-runtime-|pty-connection\/(?:remote-program-redraw|hidden-output-restore-|park-reveal-snapshot-verdict))|main\/runtime\/rpc\/methods\/terminal\/terminal-(?:multiplex-initial-snapshot|snapshot-publication)\.ts$)/.test(
file
))
},
{
// Unsaved drafts stay on the desktop and are renamed to the server when a move commits.
id: 'ssh.orcad-editor-drafts-migration',
specs: ['tests/e2e/ssh-orcad-editor-drafts-migration.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/main\/(?:persistence\/migrating-orcad-catalog\/orcad-source-(?:draft-rename|workspace-session|session-copy-resolution|export)|ssh\/orcad-migration-source-retention|ipc\/ssh\/ssh)\.ts$/.test(
file
))
},
{
// A saved managed tunnel port that can no longer be bound moves to a free one (#33182).
id: 'ssh.orcad-unbindable-tunnel-port',
specs: ['tests/e2e/ssh-orcad-unbindable-tunnel-port.spec.ts'],
matches: (file) =>
MANAGED_SSH_HARNESS.test(file) ||
(isProductSource(file) &&
/^src\/(?:main\/managed-server\/lifecycle\/orcad-managed-tunnel|main\/ssh\/system-transport\/system-ssh-forward-port-reservation|shared\/runtime-environment-(?:sidecar|managed-orcad-store))/.test(
file
))
},
{
// Why: hand-kept routes missed #30130 and #30120; scoped to managed-host code to bound lane cost.
id: 'ssh.orcad-remote-sources',
specs: [
'tests/e2e/ssh-orcad-paired-phone-projects.spec.ts',
'tests/e2e/ssh-orcad-pairing-repair.spec.ts',
'tests/e2e/ssh-orcad-retained-upgrade-state.spec.ts',
'tests/e2e/ssh-orcad-shared-home-machines.spec.ts',
'tests/e2e/ssh-orcad-shared-login-migration.spec.ts',
'tests/e2e/ssh-orcad-terminal-network-outage.spec.ts',
'tests/e2e/ssh-orcad-windows-cli-matrix.spec.ts'
],
matches: (file) =>
isProductSource(file) &&
/^src\/(?:main\/(?:ssh|orcad|managed-server)\/|main\/ipc\/(?:ssh|runtime-environment|orcad-)|cli\/runtime\/(?:launch-)?serve|shared\/(?:remote-runtime|orcad-|protocol-version\.ts$|runtime-capability)|renderer\/src\/store\/repos\/(?:repo-removal|runtime-project-terminal-stop)|renderer\/src\/components\/sidebar\/(?:sleep-|SleepTwin|WorkspaceSleep|use-worktree-sleep|ssh-host-remove|host-rename-remove|HostRemove)|renderer\/src\/components\/terminal-pane\/(?:TerminalRemoteRuntimeReconnectBanner|use-managed-pairing-refused|TerminalPaneRuntimePortals))/.test(
file
)
},
{
id: 'browser-network.ssh-docker-route',
specs: ['tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts'],
matches: (file) =>
file === 'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts' ||
/^tests\/e2e\/helpers\/docker-ssh-host-(?:image|target)\.ts$/.test(file) ||
(isProductSource(file) &&
/^src\/main\/(?:browser\/(?:ssh-browser-network-execution-route|browser-network-deferred-socket|browser-network-execution-route|system-ssh-socks-client-socket)|ssh\/system-transport\/system-ssh-dynamic-forward-process)\.ts$/.test(
file
))
},
{
id: 'wsl-host.fake-connector',
specs: ['tests/e2e/wsl-host-fake-transport.spec.ts', 'tests/e2e/wsl-host-chat.spec.ts'],
matches: (file) => file === 'src/main/wsl/wsl-host-e2e-fake-connector.ts'
},
{
// Remove runs through the fake transport spec; its peer check asks the server who else uses it.
id: 'wsl-host.remove',
specs: ['tests/e2e/wsl-host-fake-transport.spec.ts'],
matches: (file) =>
/^src\/main\/wsl\/(?:wsl-host-server-stop|wsl-host-peer-desktops|wsl-host-removal)\.ts$/.test(
file
)
},
{
id: 'terminal.windows-wsl-launch-and-paste',
specs: [
'tests/e2e/golden-tab-bar-agent-launch.spec.ts',
'tests/e2e/terminal-windows-shell-paste-ownership.spec.ts'
],
matches: (file) =>
isProductSource(file) &&
/^(?:config\/scripts\/(?:verify-wsl-e2e-participation|verify-playwright-participation)\.mjs$|src\/main\/(?:wsl[/-]|pty\/.*wsl|providers\/wsl)|src\/shared\/(?:wsl-|windows-terminal-shell|shell-argument-quoting)|src\/renderer\/src\/.*(?:terminal-paste|pty-paste)|tests\/e2e\/(?:golden-tab-bar-agent-launch\.spec|terminal-windows-shell-paste-ownership\.spec|terminal-inline-images-windows\.spec|helpers\/(?:wsl-golden-stub-agent|golden-stub-agent))|\.github\/(?:actions\/setup-wsl-test-runtime\/|workflows\/windows-wsl-e2e\.yml))/.test(
file
)
},
{
id: 'terminal-input.ime-and-synthetic-forwarding',
specs: [
'tests/e2e/terminal-cjk-ime-committed-text.spec.ts',
'tests/e2e/terminal-hangul-wrap-boundary-bytes.spec.ts',
'tests/e2e/terminal-ime-exact-byte.spec.ts',
'tests/e2e/terminal-korean-composing-chord-order.spec.ts',
'tests/e2e/terminal-korean-endofrow-preedit-cell-span.spec.ts',
'tests/e2e/terminal-korean-midline-preedit-occlusion.spec.ts',
'tests/e2e/terminal-korean-preedit-visibility.spec.ts'
],
matches: (file) =>
isProductSource(file) &&
(XTERM_CORE_PATCH.test(file) ||
/^src\/renderer\/src\/components\/terminal-pane\/(?:terminal-ime-|use-terminal-pane-lifecycle|xterm-bypass-policy|terminal-option-shortcut-policy)/.test(
file
))
},
{
// CDP Input.imeSetComposition proves synthetic handling without an IME; this separate route alone selects the real ibus-hangul lane.
id: 'terminal-ime.native-input-method',
specs: ['tests/e2e/terminal-ibus-hangul-native.spec.ts'],
matches: (file) =>
(isProductSource(file) &&
(XTERM_CORE_PATCH.test(file) || NATIVE_IME_PRODUCT_SOURCE.test(file))) ||
NATIVE_IME_HARNESS.test(file)
},
{
id: 'terminal-startup.quick-command-pre-bind-recovery',
specs: ['tests/e2e/terminal-quick-command-pre-bind-recovery.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/renderer\/src\/components\/tab-bar\/TabBarQuickCommandsMenu\.tsx|src\/renderer\/src\/hooks\/use-terminal-quick-command-hosts\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:pty-connection|pty-transport|terminal-pty-pre-spawn-e2e-barrier)\.ts|src\/renderer\/src\/components\/terminal-pane\/pty-connection\/(?:connect-pane-pty|fresh-spawn-start|pane-pty-visibility-bind|pty-input-recovery)\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:TerminalPane|use-terminal-pane-lifecycle)\.tsx?|src\/renderer\/src\/store\/slices\/terminals\.ts)$/.test(
file
)
},
{
id: 'quick-open.paired-host-path-search',
specs: ['tests/e2e/paired-quick-open-large-tree.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/ipc\/filesystem-(?:list-files|search-file-paths)\.ts|src\/main\/ripgrep\/bundled-ripgrep-path\.ts|src\/main\/providers\/(?:filesystem-provider-contract|ssh-filesystem-provider(?:-capabilities)?)\.ts|src\/main\/runtime\/(?:orca-runtime-files|rpc\/methods\/files)\.ts|src\/renderer\/src\/(?:components\/(?:QuickOpen|quick-open-file-list|quick-open-search)\.tsx?|runtime\/(?:runtime-file-search-client|runtime-legacy-quick-open-inventory)\.ts)|src\/shared\/(?:quick-open-(?:install-rg|path-search|transport-budget)|ripgrep-process-availability|bundled-ripgrep)\.ts)$/.test(
file
)
},
{
id: 'terminal-session.host-cold-park-stream-continuity',
specs: ['tests/e2e/host-parked-pane-remote-viewer.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/renderer\/src\/components\/terminal-pane\/(?:terminal-hidden-view-parking|terminal-tab-park-candidates|terminal-tab-activation-order|terminal-parked-pty-watcher|terminal-parked-tab-watchers|terminal-parked-watcher-registry)\.ts|src\/renderer\/src\/runtime\/sync-runtime-graph\.ts)$/.test(
file
)
},
{
// Route unbind/retirement as well as binders: missed lifecycle edges can strand PTYs or leave blank mounted leaves.
// Omit runtime-rpc-client.ts: it makes no close decision and churns ~3x as often, triggering unrelated work.
id: 'terminal-pane.close-and-retirement',
specs: [
// Closing a tab whose pane is parked (never mounted) must retire that exact PTY.
'tests/e2e/terminal-parked-close-retirement.spec.ts',
// Split-leaf close must preserve agreement among root leaves, leaf-to-PTY bindings and live panes to prevent ghost blanks.
'tests/e2e/terminal-pane-close-layout-consistency.spec.ts',
// The runtime half: a leaf the host retires must stop being mounted on a paired client.
'tests/e2e/paired-remote-split-pane-host-retired-ghost.spec.ts'
],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/renderer\/src\/components\/terminal-pane\/(?:retire-unbound-(?:ipc|runtime)-terminal-pane|terminal-pane-(?:close-admission|close-identity|lifecycle-close|pane-closed|retirement-ownership)|use-terminal-pane-close-actions)|src\/renderer\/src\/store\/(?:terminals\/terminal-tab-close(?:-providers)?|slices\/(?:terminal-tab-retirement|terminal-retirement-teardown-reservation|retired-terminal-tab-state-sweep)))\.ts$/.test(
file
)
},
{
id: 'terminal-session.parked-cli-split',
specs: ['tests/e2e/terminal-parked-cli-split.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/window\/attach-main-window-services\.ts|src\/preload\/(?:index|api\/ui-command-event-api)\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:terminal-pane-split-request-routing|use-terminal-pane-lifecycle|use-terminal-tab-cold-parking)\.ts|src\/renderer\/src\/hooks\/ipc-events\/terminal-ui-routing-ipc-bridge\.ts)$/.test(
file
)
},
{
id: 'terminal-session.paired-serve-restart-binding-continuity',
specs: ['tests/e2e/paired-remote-terminal-serve-restart-binding.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/main\/daemon\/(?:daemon-attach-only-retirement|daemon-pty-applied-size|daemon-pty-session-control|daemon-pty-spawn-result)\.ts|src\/renderer\/src\/components\/terminal-pane\/(?:remote-runtime-pty-transport|terminal-error-accumulation)\.ts|src\/renderer\/src\/runtime\/(?:web-runtime-terminal-creation|web-runtime-session-tab-lifecycle|web-session-tabs-sync\/use-web-session-tabs-sync|web-session-terminal-orphan-(?:topology|recovery(?:-(?:adoption|surface|inventory|inventory-validation|cache|queue|rpc-lane|pane))?))\.ts)$/.test(
file
)
},
{
id: 'terminal-provider.ssh-remote-reattach-contract',
specs: ['tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts'],
matches: (file) =>
isProductSource(file) &&
!file.endsWith('-test-harness.ts') &&
/^(?:src\/renderer\/src\/components\/terminal-pane\/remote-runtime-pty-transport(?:-[a-z0-9-]+)?\.ts|src\/renderer\/src\/runtime\/remote-runtime-terminal-multiplexer\.ts)$/.test(
file
)
},
{
// Layout resolution can invent split directions; publishing the guess overwrites the original tree irreversibly.
id: 'terminal-session.split-orientation-resolution',
specs: ['tests/e2e/desktop-published-split-orientation-legacy-leaf.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/renderer\/src\/runtime\/(?:remote-terminal-layout-resolution\.ts|sync-runtime-graph\/(?:graph-publication|mobile-session-terminal-tabs|mobile-session-surfaces)\.ts|web-session-tabs-sync\/terminal-surfaces\.ts)$/.test(
file
)
},
{
id: 'terminal-session.remote-pane-layout-retry',
specs: ['tests/e2e/paired-remote-pane-layout-retry.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^(?:src\/renderer\/src\/components\/terminal-pane\/(?:remote-pane-layout-push|TerminalPane)\.tsx?|src\/renderer\/src\/lib\/terminal-layout-equality\.ts|src\/renderer\/src\/runtime\/web-session-tabs-sync\/use-web-session-tabs-sync\.ts|src\/renderer\/src\/store\/slices\/terminals\.ts)$/.test(
file
)
},
{
// Connector code only: the shared launcher, writer and hook store it reuses already have their own routes.
id: 'connectors.headless-serve-conversation',
specs: ['tests/e2e/connector-headless-serve.spec.ts'],
matches: (file) =>
file === 'tests/e2e/helpers/connector-fake-claude.ts' ||
(isProductSource(file) &&
/^src\/(?:main\/external-conversations\/|main\/runtime\/rpc\/methods\/connector\.ts$|cli\/(?:handlers|specs)\/connector\.ts$|shared\/rpc-contract\/connector-params\.ts$)/.test(
file
))
}
]
export function selectPrE2eSpecs(
changedPaths,
reportRoute = () => undefined,
{ allowDaemonInitTestSupport = false } = {}
) {
const routePaths = isDaemonInitTestSupportChange(changedPaths, allowDaemonInitTestSupport)
? changedPaths.filter((file) => file !== DAEMON_INIT_TEST_SUPPORT)
: changedPaths
// The required layout_oracle job owns the oracle specs; running them here too would double the cost.
const specs = new Set(
changedPaths.filter(
(file) =>
/^tests\/e2e\/.*\.spec\.ts$/.test(file) &&
!LAYOUT_ORACLE_E2E_SPECS.includes(file) &&
!MANUAL_ONLY_E2E_SPECS.includes(file)
)
)
for (const route of PR_E2E_SOURCE_ROUTES) {
const matchedFiles = routePaths.filter(route.matches)
if (matchedFiles.length === 0) {
continue
}
route.specs.forEach((spec) => specs.add(spec))
reportRoute(`[pr-e2e] ${route.id}: ${route.specs.join(', ')}`)
}
if (
routePaths.some(
(file) =>
(file.startsWith('tests/e2e/helpers/') &&
[...specs].some((spec) => spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX))) ||
file === '.github/workflows/e2e.yml' ||
file === '.github/scripts/prepare-ubuntu-apt.sh' ||
file === 'config/scripts/ci-e2e-job-selection.mjs'
)
) {
specs.add(MANAGED_SSH_E2E_SPEC_PREFIX)
}
return [...specs].sort((left, right) => left.localeCompare(right))
}
/** Routes whose authorities a real input method can judge, and so require the native IME lane. */
export const NATIVE_IME_SOURCE_ROUTE_IDS = ['terminal-ime.native-input-method']
// Derive native-IME triggers from source routes, independent of whether a route still lists the native spec.
export function hasNativeImeSourceChange(changedPaths) {
return PR_E2E_SOURCE_ROUTES.filter((route) =>
NATIVE_IME_SOURCE_ROUTE_IDS.includes(route.id)
).some((route) => changedPaths.some(route.matches))
}
export function shouldRunReusablePrE2e(changedPaths, options = {}) {
// Native IME has its own workflow.
return selectPrE2eSpecs(changedPaths, undefined, options).some(
(spec) => spec !== 'tests/e2e/terminal-ibus-hangul-native.spec.ts'
)
}
export function hasWslSourceChange(changedPaths) {
const route = PR_E2E_SOURCE_ROUTES.find(
(candidate) => candidate.id === 'terminal.windows-wsl-launch-and-paste'
)
return changedPaths.some(route.matches)
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
let input = ''
process.stdin.setEncoding('utf8')
for await (const chunk of process.stdin) {
input += chunk
}
const changedPaths = input.split(/\r?\n/).filter(Boolean)
const options = {
allowDaemonInitTestSupport: process.argv.includes('--daemon-init-test-support')
}
if (process.argv.includes('--reusable-workflow')) {
process.stdout.write(`${shouldRunReusablePrE2e(changedPaths, options)}\n`)
} else if (process.argv.includes('--wsl-source')) {
process.stdout.write(`${hasWslSourceChange(changedPaths)}\n`)
} else if (process.argv.includes('--native-ime-source')) {
process.stdout.write(`${hasNativeImeSourceChange(changedPaths)}\n`)
} else {
const specs = selectPrE2eSpecs(changedPaths, (message) => console.error(message), options)
process.stdout.write(`${JSON.stringify(specs)}\n`)
}
}