Files
orca/config/scripts/ci-e2e-job-selection.mjs
T
OrcaWinandm4air 36b8cd81c7 feat(orcad): managed orcad idles out after 15 minutes and is started again whenever it is down (#25121)
* feat(orcad): a managed orcad stops after 15 idle minutes and starts again on the next connect

A client-launched orcad now exits, like the relay, once no client, terminal,
working agent, staged migration or activation fence has been seen for 15
minutes. The exit is the normal graceful shutdown, which leaves the terminal
daemon running; the daemon retires only if it proves itself empty. A record in
the data root tells the next start (and its readiness health) that the stop
was an idle one rather than a crash.

On connect and after host resume, a fresh tunnel whose server does not answer
starts the activated slot under the activation fence, but only on a proven
exit, so a stopped server reads as not running rather than a failure.

* fix(orcad): keep orcad-entry under max-lines; idle e2e connects without a relay repo

* fix(orcad): deploy and rollback launches carry the managed idle-exit fence

The candidate launch in activation and the rollback launch built their
own launch spec without the activation root, so a freshly deployed orcad
never enabled idle exit; only the wake path did. The field is now required
on every launch spec, so the type system covers each launch site.

* feat(ssh): start a stopped managed orcad on connect, on restore and after resume

Once orcad stopped (idle, kill or host reboot), a connect still resolved
managed over a forward to a dead port and every call failed. Every connect
now checks the server behind its tunnel, as does a call through a restored
environment; a server proven stopped is started from its activated slot
under the activation fence, adopting a surviving daemon and its terminals.
The status line shows the start, and a start that fails keeps the host
managed with the reason and orcad.log's tail, never as a terminal verdict.

* fix(ssh): reuse a serving verdict only on the same SSH transport, for 5s

A reconnect right after a reboot was answered from the previous
transport's cached verdict, so the stopped server was never started.

* fix(ssh): key the serving verdict on the tunnel's remote port too

* test(ssh): a stopped server starts before the update counts its terminals

* feat(ssh): check serving at the bound port, and follow a restarted orcad to a new one

The serving check uses the port the tunnel forwards to (the one orcad bound).
A restart that binds a different port drops the forward and rebuilds it at
the new port, within the same ensure or on the explicit connect check. The
tunnel manager class moves to its own file to stay under max-lines.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
2026-10-04 13:57:56 -07:00

121 lines
4.6 KiB
JavaScript

import { pathToFileURL } from 'node:url'
export const DOCKER_SSH_E2E_SPECS = [
'tests/e2e/local-ssh-browser-routing.spec.ts',
'tests/e2e/ssh-client-hosted-browser-drop-reconnect.spec.ts',
'tests/e2e/pty-input-write-queue-ssh.spec.ts',
'tests/e2e/ssh-ai-vault-session-history.spec.ts',
'tests/e2e/ssh-codex-display-artifacts-repro.spec.ts',
'tests/e2e/ssh-cold-activation-restore.spec.ts',
'tests/e2e/ssh-cold-hydration-gap-tab-seeding.spec.ts',
'tests/e2e/ssh-docker-five-pane-input-under-flood.spec.ts',
'tests/e2e/ssh-docker-bulk-open-freeze-repro.spec.ts',
'tests/e2e/ssh-docker-half-open-link.spec.ts',
'tests/e2e/ssh-docker-quick-open-large-listing.spec.ts',
'tests/e2e/ssh-docker-reconnect-pane-restore.spec.ts',
'tests/e2e/ssh-docker-relay-stall-credential.spec.ts',
'tests/e2e/ssh-docker-resource-accumulation.spec.ts',
'tests/e2e/ssh-docker-transport-drop-recovery.spec.ts',
'tests/e2e/ssh-external-image-preview.spec.ts',
'tests/e2e/ssh-lost-kill-tab-resurrection.spec.ts',
'tests/e2e/ssh-pi-compatible-agent-title.spec.ts',
'tests/e2e/ssh-port-forward-lifecycle.spec.ts',
'tests/e2e/ssh-reconnect-tab-destruction.spec.ts',
'tests/e2e/ssh-restart-tab-accumulation.spec.ts',
'tests/e2e/ssh-skill-installation.spec.ts',
'tests/e2e/ssh-stale-resume-execution-host-scope.spec.ts',
'tests/e2e/ssh-terminal-window-wake-stale-grid-repro.spec.ts',
'tests/e2e/terminal-inline-images-ssh.spec.ts',
'tests/e2e/ssh-docker-watcher-isolation.spec.ts',
'tests/e2e/ssh-terminal-parking.spec.ts',
'tests/e2e/terminal-retention-budget.spec.ts',
'tests/e2e/ssh-startup-exec-readiness.spec.ts',
'tests/e2e/paired-startup-exec-readiness.spec.ts'
]
export const NODE_NETWORK_E2E_SPEC =
'tests/e2e/ssh-browser-network-execution-route.docker.unit.test.ts'
export const LOCALHOST_SSH_E2E_SPEC = 'tests/e2e/ssh-localhost.spec.ts'
export const NATIVE_IME_E2E_SPEC = 'tests/e2e/terminal-ibus-hangul-native.spec.ts'
// Needs the packaged orcad slot, which only its own job builds.
export const ORCAD_SERVE_MODE_SWITCH_E2E_SPEC = 'tests/e2e/orcad-serve-mode-switch.spec.ts'
// Needs the orcad template for its host's target, which only its own job builds.
export const ORCAD_AUTO_CONVERT_E2E_SPEC = 'tests/e2e/ssh-orcad-auto-convert.spec.ts'
// Windows-only; its own job runs it on a Windows runner.
export const WINDOWS_MISSING_APPDATA_E2E_SPEC = 'tests/e2e/windows-missing-appdata-startup.spec.ts'
// Runs in the auto-convert job, which builds the template it needs.
export const ORCAD_IDLE_EXIT_E2E_SPEC = 'tests/e2e/ssh-orcad-idle-exit.spec.ts'
export const DEDICATED_E2E_SPECS = [
...DOCKER_SSH_E2E_SPECS,
NODE_NETWORK_E2E_SPEC,
LOCALHOST_SSH_E2E_SPEC,
NATIVE_IME_E2E_SPEC,
ORCAD_SERVE_MODE_SWITCH_E2E_SPEC,
ORCAD_AUTO_CONVERT_E2E_SPEC,
WINDOWS_MISSING_APPDATA_E2E_SPEC,
ORCAD_IDLE_EXIT_E2E_SPEC
]
const dedicatedSpecs = new Set(DEDICATED_E2E_SPECS)
const dockerSpecs = new Set(DOCKER_SSH_E2E_SPECS)
export function selectGeneralE2eSpecs(specs) {
return specs.filter((spec) => !dedicatedSpecs.has(spec))
}
function parseSpecs(input) {
const specs = JSON.parse(input)
if (!Array.isArray(specs) || specs.some((spec) => typeof spec !== 'string' || !spec)) {
throw new Error('Expected a JSON array of nonempty E2E spec paths')
}
return specs
}
export function classifyE2eJobs(input, sshSourceChanged = 'false') {
const conservative = { e2e_run_changed: true, e2e_needs_build: true }
let specs
try {
specs = parseSpecs(input)
} catch {
return conservative
}
// Empty evidence keeps allocations; the consumer still validates its input.
if (specs.length === 0) {
return conservative
}
const runChanged = selectGeneralE2eSpecs(specs).length > 0
return {
e2e_run_changed: runChanged,
e2e_needs_build:
runChanged ||
sshSourceChanged !== 'false' ||
specs.some(
(spec) =>
dockerSpecs.has(spec) ||
spec === LOCALHOST_SSH_E2E_SPEC ||
spec === ORCAD_SERVE_MODE_SWITCH_E2E_SPEC
)
}
}
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
}
if (process.argv.includes('--job-outputs')) {
for (const [name, value] of Object.entries(
classifyE2eJobs(input, process.env.E2E_SSH_SOURCE_CHANGED ?? 'false')
)) {
process.stdout.write(`${name}=${value}\n`)
}
} else {
for (const spec of selectGeneralE2eSpecs(parseSpecs(input))) {
if (/[\r\n]/.test(spec)) {
throw new Error('E2E spec paths cannot contain newlines')
}
process.stdout.write(`${spec}\n`)
}
}
}