Files
orca/config/scripts/pr-e2e-source-routing.mjs
T
Neil 045f8c8266 feat(files): show images and PDFs from the byte-range preview path, local and remote (#35165)
* feat(files): show images and PDFs from the byte-range preview path, local and remote

ImageViewer and PdfViewer read an orca-media:// URL instead of base64 when
the editor opens a file: images load by URL, and pdf.js reads PDFs by
range, so a large PDF no longer arrives whole through files.readPreview or
pages into memory first. The grant mints only for the editor's viewers;
diffs, git blobs, markdown images and the paired web client keep base64.

- Minting an image checks its header with the base64 path's rule, so an
  image proven too large to decode safely never reaches the decoder.
- orca-media gains fetch and CORS privileges, and range responses expose
  Accept-Ranges and Content-Range, so pdf.js can read by range.
- Grants report the file size the viewer used to estimate from base64.

* fix(files): read PDFs by real range, check served images, keep old hosts' previews

- pdf.js fetches ranges only over http(s), so an orca-media URL was read
  whole; a small range transport now hands it bytes=a-b reads from the
  grant, and a failed range fails the preview instead of hanging it.
- A file can change after mint, so the raster header is checked again on
  the bytes each image request serves (the same handle locally).
- A paired host without files.readChunk falls back to files.readPreview
  for images and PDFs, as before; only audio and video need the update.

* fix(files): mint no local document grant for a renderer gone mid-mint

The local image/PDF mint is async now (authorize, open, header check), so
a renderer that closed meanwhile could leave a grant no teardown drops. It
binds the renderer lifetime first and refuses to mint once it's gone, as
the remote mint does.

* fix(files): keep the 50 MB cap for URL images, and probe remote headers incrementally

An <img> decodes the whole file, so image grants keep the base64 path's
50 MB cap; PDFs, now read by range, don't need one. A remote header probe
reads only each wider step's new bytes instead of re-reading from the
start. A replaced image of a different size resets its zoom.

* fix(files): tear down a PDF load whose range failed, and read 256 KiB ranges

A failed range only showed the error and left the pdf.js task, its worker
and pages alive; it now destroys that load first. pdf.js asks for one
readChunk-sized range at a time instead of 64 KiB, and a Range preflight
gets its CORS answer instead of 405.

* fix(files): read PDF ranges over IPC so orca-media needs no fetch or CORS

orca-media had gained supportFetchAPI and corsEnabled, and its responses
sent Access-Control-Allow-Origin: *, so pdf.js could fetch ranges. That let
any default-session frame holding a token read the bytes. pdf.js now reads
a PDF grant's ranges through fs:readPreviewRange, which main answers only
for the renderer that minted the token. The scheme is back to element-load
privileges (standard, secure, stream), responses carry no CORS headers, and
preflights get 405.

Grant bookkeeping moves to preview-grant-registry.ts. An e2e spec proves an
orca-media URL can't be fetched from the app page or read from another
session.

* ci(e2e): route the byte-range preview sources to the viewer specs that exercise them

Changes to the orca-media grant path, the image/PDF viewers and the PDF
range transport now run the PDF reload, rebuild and find specs, the remote
large-file open, media playback and the partition-isolation spec.

* fix(files): stop PDF range reads on abort, abort remote reads with the window, fix the CI route

- A closed or reloaded viewer schedules no further pieces of a merged range,
  and a remote range read stops when its window goes away.
- A URL without a size is an error rather than a fetch the scheme refuses.
- The PR guard job's sparse checkout and its admission test include the new
  route module, and the route now also covers the mint handler, preload
  bridge, editor surfaces, scheme registration and read client.

* fix(files): classify linked previews by their target, keep range reads off stopped WSL distros

A local image or PDF reached through a link is classified by the file it
resolves to, at mint and on every read, and a re-mint after the link is
retargeted gets a fresh grant of the new type. A paired host's document
whose first bytes are another kind defers to files.readPreview, which
classifies the target there. Lazy local reads go through the non-waking
WSL guard, as fs:readFile and fs:readFileChunk do.
2026-10-28 09:31:22 -07:00

640 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'
import { PR_EDITOR_PREVIEW_E2E_SOURCE_ROUTES } from './pr-editor-preview-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,
...PR_EDITOR_PREVIEW_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`)
}
}