Files
orca/config/scripts/pr-e2e-source-routing.mjs
T
Neil ccedfe312e fix(ssh): move enabled SSH automations to the host's Orca server (#34044)
* fix(ssh): move enabled SSH automations to the host's Orca server

Connecting an SSH host moves its saved state to the host's managed Orca
server. An enabled SSH automation refused that move on every Connect
("automations (1), automation runs (4)"), leaving the host unconnectable;
Move anyway connected it but stranded the automation on the desktop, where
it no longer ran.

An enabled automation whose runs have all finished now moves with its run
history. The manifest still carries it disabled, which every server version
accepts; once the move commits the desktop disables its retained copy and
turns the server's copy back on through automation.update, re-sending its
start time so the next run is the next future occurrence. A server that
can't is noted in Settings; an unreachable one is retried on the next
Connect. While a move fences the host, the desktop scheduler, Run now and
the launch re-check write nothing for its automations, so a tick can no
longer break the fenced export; releasing the fence resumes them. A run
still in flight keeps blocking and the refusal now says to wait for it.

Fixes #29083

* test(ssh): mock the server automation enabler in the delta-move handler test

* test(e2e): keep the Move anyway specs refusing now that enabled automations move

The inline Move anyway spec seeded an enabled automation to force a
refusal, which this branch now moves instead. It seeds one pinned to an
earlier registration of the host, which still stays behind. The relay
bridge spec's Move anyway lookup also matched the refusal toast's
button since #34041; it now takes the pane's own.
2026-10-26 16:11:06 -07:00

641 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'
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 = [
{
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: '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
)
},
{
// Client-rendered host rows need two real Electron apps; omit high-churn ipc/runtime/useIpcEvents/preload seams covered by main-process integration.
id: 'client-hosted-browser.host-strip',
specs: ['tests/e2e/paired-client-hosted-browser-host-strip.spec.ts'],
matches: (file) =>
isProductSource(file) &&
/^src\/.*(?:[Cc]lient-?[Hh]osted-?[Bb]rowser|BrowserPaneOverlayLayer)/.test(file)
},
{
// Restart survival also depends on adoption, lease/reconciliation and tab snapshots; include orca-runtime.ts despite churn because it publishes the hold flag.
id: 'client-hosted-browser.restart-survival',
specs: ['tests/e2e/paired-client-hosted-browser-restart-survival.spec.ts'],
matches: (file) =>
isProductSource(file) &&
(file === 'src/main/runtime/browser/browser-commands.ts' ||
/^src\/.*(?:[Cc]lient-?[Hh]osted|browser-host-(?:lease|page|client-page)|browser-client-(?:host|page)|runtime-browser-(?:client-)?page|session-tabs-sync|host-session-snapshot-authority|orca-runtime\.ts|\/runtime-(?:status|types)\.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`)
}
}