Files
orca/config/scripts/orcad-e2e-change-scope.test.mjs
T
Neil ab9532ecd8 feat(wsl): let an idle WSL distro stop once its server idle-exits (#33084)
* feat(wsl): let an idle WSL distro stop once its orcad idle-exits

The WSL host's holder kept the distro (and the WSL VM) running after Orca quit, even with
nothing running in it. orcad now leaves a release request in ~/.orca when a managed idle stop
retired its terminal daemon; the holder waits for that orcad to exit, claims the request and
ends, so WSL can stop the distro. Live terminals, working agents, enabled automations or a
returning client keep the distro held as before. A host whose distro is stopped at startup
reads as stopped instead of unreachable, and the real-WSL lane gains a phase that proves the
release and records memory.

* fix(wsl): key the holder release on the WSL-host launch marker; name the lane phase

The real-WSL lane showed orcad idle-stopping with its daemon retired while the distro stayed
up: the setsid'd orcad's environment is not guaranteed to carry WSL_DISTRO_NAME, so the
release request was never written. Use ORCA_WSL_ORCAD_HOST, which only WSL-host launches set.
Phase 3 now carries its phase name and path filter, and records what kept a distro running.

* test(wsl): dispose the idle-release lane's guest transports

Each guest command left its exec loop running, a wsl.exe client that kept the distro up after
the holder had already let go (the receipt showed no holder, no orcad, no request left).

* test(wsl): record which wsl.exe clients keep the idle-release lane's distro up

* test(wsl): satisfy the flaky-test ratchet; feed the new idle port in the release test

* test(wsl): snapshot guest processes after launch to find what holds the session

* test(wsl): locate the step that leaves the zombie holding the idle distro

* test(wsl): log the idle-release lane's guest commands with times to place the zombie

* test(wsl): follow the remote runtime client's move

* test(wsl): record when each leftover WSL session init started

* fix(wsl): keep the distro while the user still runs anything, and never trip on the request file

Review of #33084:
- The holder now checks, before claiming a release, that no process of the user runs outside
  the holder itself (session plumbing aside: systemd --user, (sd-pam), dbus-daemon). A detached
  tmux, nohup job or dev server keeps the distro; it fails toward holding.
- A new holder drops any request left from before it started, so an older orcad's request can
  never end a holder a client just asked for.
- orcad touches the request file only for WSL-host launches and never throws over it.
- The real-WSL lane proves a detached job keeps the holder and its request, and the distro
  stops once the job ends.

* fix(wsl): a running container keeps the distro, whoever owns its shim

Docker's containerd-shim processes run as root, so the user-only scan missed them and a
release would have stopped running containers. Any containerd-shim process now keeps the hold.

* fix(wsl): read each process status only up to Uid; document what keeps the hold

Delta review of #33084: dash reads a status file a byte at a time, so stop at Uid. Document
that a root default user never releases, that per-user agents hold it, and the hidepid
exception. The lane's pkill no longer matches the shell running it.
2026-10-27 01:28:40 -07:00

243 lines
9.3 KiB
JavaScript

import { existsSync, readFileSync } from 'node:fs'
import { matchesGlob } from 'node:path'
import { describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
const workflows = {
windows: parse(
readFileSync(
new URL('../../.github/workflows/win-orcad-serve-switch-e2e.yml', import.meta.url),
'utf8'
)
),
wsl: parse(
readFileSync(new URL('../../.github/workflows/wsl-orcad-e2e.yml', import.meta.url), 'utf8')
),
computer: parse(
readFileSync(new URL('../../.github/workflows/computer-e2e.yml', import.meta.url), 'utf8')
),
hostileHosts: parse(
readFileSync(new URL('../../.github/workflows/ssh-hostile-hosts.yml', import.meta.url), 'utf8')
),
windowsHosts: parse(
readFileSync(new URL('../../.github/workflows/ssh-windows-hosts.yml', import.meta.url), 'utf8')
)
}
function workflowIncludes(paths, files) {
return files.some((file) =>
paths.reduce((included, pattern) => {
const excluded = pattern.startsWith('!')
return matchesGlob(file, excluded ? pattern.slice(1) : pattern) ? !excluded : included
}, false)
)
}
const cases = {
windows: {
excluded: [
'src/main/daemon/daemon-server.test.ts',
'src/main/orcad/orcad-native-preflight.test.ts',
'src/main/orcad/nested/orcad-launch.test.ts',
'src/cli/runtime/serve.test.ts',
'src/cli/runtime/nested/serve.test.ts'
],
retained: [
'src/main/daemon/daemon-entry.ts',
'src/main/daemon/daemon-init-dependency-mocks.ts',
'src/main/orcad/main.ts',
'src/main/orcad/launcher.ts',
'src/main/orcad/orcad-node-slot-fixture.ts',
'src/main/orcad/orcad.test-fixture.ts',
'src/main/orcad/__fixtures__/fake-orcad-electron-sidecar.cjs',
'src/main/orcad/orcad.test.json',
'src/cli/runtime/serve.ts',
'src/cli/runtime/serve-test-harness.ts',
'src/shared/orcad-local-serve-selection.ts',
'tests/tools/win-update-e2e/serve-switch.mjs',
'.github/workflows/win-orcad-serve-switch-e2e.yml'
],
product: 'src/main/orcad/main.ts'
},
wsl: {
excluded: [
'src/main/wsl/wsl-orcad-deploy.test.ts',
'src/main/wsl/nested/wsl-orcad-deploy.test.ts',
'src/main/ssh/orcad-activation-outcome-log.test.ts',
'src/main/managed-server/install/orcad-remote-shell-commands.integration.test.ts',
'src/packages/process-host/src/run-process.test.ts'
],
retained: [
'src/main/wsl/wsl-orcad-real-distro-lane.test.ts',
'src/main/wsl/wsl-orcad-deploy.ts',
'src/main/runtime/runtime-rpc.ts',
'src/main/claude/claude-structured-session-state.ts',
'src/shared/agent-session-wire-refusals.ts',
'package.json',
'pnpm-lock.yaml',
'src/main/wsl/wsl-orcad-bridge-test-child.ts',
'src/main/wsl/wsl-orcad.test-fixture.ts',
'src/main/wsl/wsl-orcad.test.json',
'src/main/managed-server/install/orcad-remote-launch.ts',
'src/main/ssh/orcad-launch-test-harness.ts',
'src/main/ssh/orcad-launch.test-fixture.ts',
'src/main/managed-server/install/remote-install-transport.ts',
'src/packages/process-host/src/run-process.ts',
'.github/workflows/wsl-orcad-e2e.yml'
],
product: 'src/main/wsl/wsl-orcad-deploy.ts'
}
}
for (const [lane, { excluded, retained, product }] of Object.entries(cases)) {
describe(`${lane} Orcad E2E change routing`, () => {
const workflow = workflows[lane]
const paths = workflow.on.pull_request.paths
it.each(excluded)('omits a changed or deleted unit root: %s', (file) => {
expect(workflowIncludes(paths, [file])).toBe(false)
expect(workflowIncludes(paths, [file, 'docs/ci.md'])).toBe(false)
expect(workflowIncludes(paths, [file, product])).toBe(true)
// A rename retains both names, including its product source or destination.
expect(workflowIncludes(paths, [file, file.replace('.test.ts', '.ts')])).toBe(true)
expect(workflowIncludes(paths, [file.replace('.test.ts', '.ts'), file])).toBe(true)
})
it.each(retained)('retains a changed or deleted lane input: %s', (file) => {
expect(workflowIncludes(paths, [file])).toBe(true)
})
it('keeps explicit manual runs available', () => {
expect(workflow.on).toHaveProperty('workflow_dispatch')
})
})
}
describe.each(['windows', 'computer'])('%s unit-support routing', (lane) => {
const paths = workflows[lane].on.pull_request.paths
const helpers = [
'src/main/daemon/serialize-grid-roundtrip.ts',
'src/main/daemon/mock-descendant-sweep.ts',
'src/main/daemon/daemon-host-relocation.test-fixture.ts',
'src/main/daemon/daemon-endpoint-reachability-test-harness.ts'
]
const product = 'src/main/daemon/daemon-entry.ts'
const renamed = 'src/main/daemon/serialize-grid-runtime.ts'
it.each(helpers)('leaves known unit support to its suites: %s', (helper) => {
expect(workflowIncludes(paths, [helper])).toBe(false)
expect(workflowIncludes(paths, [helper, 'docs/ci.md'])).toBe(false)
expect(workflowIncludes(paths, [helper, product])).toBe(true)
// Both names of a rename must retain the production workflow.
expect(workflowIncludes(paths, [helper, renamed])).toBe(true)
expect(workflowIncludes(paths, [renamed, helper])).toBe(true)
expect(workflowIncludes(paths, ['src/main/daemon/unknown-test-harness.ts'])).toBe(true)
})
it('keeps production selected when multiple unit fixtures change', () => {
expect(workflowIncludes(paths, helpers)).toBe(false)
expect(workflowIncludes(paths, [...helpers, product])).toBe(true)
})
})
it.each([
'config/scripts/daemon-boot-smoke.mjs',
'config/scripts/daemon-endpoint-handover-smoke.mjs',
'config/scripts/windows-daemon-workspace-close-repro.mjs',
'config/scripts/computer-use-smoke.test.mjs',
'src/main/providers/windows-shell-preflight-runtime.windows.test.ts',
'native/computer-use-macos/Sources/OrcaComputerUseMacOS/main.swift',
'src/cli/handlers/computer.ts',
'.github/workflows/computer-e2e.yml'
])('keeps the computer contract owner selected: %s', (file) => {
expect(workflowIncludes(workflows.computer.on.pull_request.paths, [file])).toBe(true)
})
it('re-includes the real WSL distro root after excluding unit tests', () => {
const paths = workflows.wsl.on.pull_request.paths
const exclude = paths.indexOf('!**/*.test.ts')
expect(exclude).toBeGreaterThan(paths.indexOf('src/main/wsl/**'))
expect(paths.indexOf('src/main/wsl/wsl-orcad-real-distro-lane.test.ts')).toBeGreaterThan(exclude)
const steps = workflows.wsl.jobs.wsl2.steps
const phases = steps.filter((step) => step.env?.ORCA_RUN_WSL_ORCAD_LANE === '1')
expect(phases.map((step) => step.env.ORCA_WSL_ORCAD_LANE_PHASE)).toEqual([
'launch',
'survive',
'idle-release'
])
for (const phase of phases) {
expect(phase.run).toMatch(
/src\/main\/wsl\/wsl-orcad-real-distro-(idle-release-)?lane\.test\.ts/
)
}
expect(
paths.indexOf('src/main/wsl/wsl-orcad-real-distro-idle-release-lane.test.ts')
).toBeGreaterThan(exclude)
})
// Each remote host lane executes these sources, so a change to one must start the lane.
describe.each([
[
'hostileHosts',
[
'src/main/ssh/connection/ssh-connection.ts',
'src/main/ssh/connection/ssh-connect-config.ts',
'src/main/ssh/ssh-posix-command-wrapper.ts',
'src/shared/shell-argument-quoting.ts'
]
],
[
'windowsHosts',
[
'src/main/ssh/connection/ssh-connection.ts',
'src/main/ssh/connection/ssh-connect-config.ts',
'src/main/ssh/ssh-posix-command-wrapper.ts',
'src/shared/shell-argument-quoting.ts'
]
],
['wsl', ['src/main/orcad/orcad-wsl-host-login-environment.ts']]
])('%s remote transport routing', (lane, sources) => {
const paths = workflows[lane].on.pull_request.paths
it.each(sources)('starts the lane for %s', (file) => {
expect(existsSync(new URL(`../../${file}`, import.meta.url)), file).toBe(true)
expect(workflowIncludes(paths, [file])).toBe(true)
expect(workflowIncludes(paths, [file.replace(/\.ts$/, '.test.ts')])).toBe(false)
})
})
describe('WSL host coverage without UI-only admissions', () => {
const paths = workflows.wsl.on.pull_request.paths
const mobileFiles = [
'mobile/src/session/MobileSessionTabStrip.tsx',
'mobile/src/session/mobile-session-frame-styles.ts'
]
const uiFiles = [
...mobileFiles,
'src/renderer/src/components/native-chat/NativeChatToolRun.tsx',
'src/renderer/src/lib/agent-session-continuation.ts'
]
it.each(uiFiles)('leaves UI-only changes to their own checks: %s', (file) => {
expect(existsSync(new URL(`../../${file}`, import.meta.url)), file).toBe(true)
expect(workflowIncludes(paths, [file])).toBe(false)
})
it('still builds and checks the mobile page for the tab-strip regression example', () => {
expect(workflowIncludes(paths, mobileFiles)).toBe(false)
expect(classifyPrJobs(mobileFiles).mobile_web_app).toBe(true)
})
it.each([
'src/main/wsl/wsl-orcad-deploy.ts',
'src/main/runtime/runtime-rpc.ts',
'src/main/claude/claude-structured-session-state.ts',
'src/shared/agent-session-wire-refusals.ts'
])('retains mixed UI and host changes, including renames: %s', (hostFile) => {
expect(workflowIncludes(paths, uiFiles)).toBe(false)
expect(workflowIncludes(paths, [...uiFiles, hostFile])).toBe(true)
expect(workflowIncludes(paths, [hostFile, ...uiFiles])).toBe(true)
})
})