You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The largest maintenance surface on the Apple platform is not geometry, it is the daemon-side cluster that babysits the XCTest runner process: packages/platform-apple/src/runner/ is 38 production files, about 10k lines. A survey on main at 91652a8fc5 found the following (file:line refer to that head):
Three response decoders for one wire.parseRunnerResponsePayload in runner-session.ts:973 (the canonical one), parseLifecycleResponsePayload in runner-command-recovery.ts:353 (the status recovery probe), and an inline JSON.parse in runner-adoption.ts:140 (the uptime probe).
Four command-encode paths.runner-transport.ts:48 (sendRunnerCommandOnce, host TCP / usbmux), runner-usbmux.ts:52 (raw HTTP framing over the usbmux socket), runner-startup-transport.ts:371 (tryRunnerEndpoints, the startup connect probe) and runner-startup-transport.ts:418 (postCommandViaSimulator: simctl spawn <udid> /usr/bin/curl …, a shell-out inside the Simulator, still reachable from lines 119 and 400).
Six notions of "alive".isRunnerProcessAlive / isRunnerProcessTreeAlive / runnerSessionsStillAlive in runner-disposal.ts:274/269/177; hasLiveIosRunnerSession in runner-client.ts:192 reading getRunnerSessionSnapshot in runner-session.ts:442; probeRunnerAnswersUptime in runner-adoption.ts:127 (wire-level); and canSkipRunnerReadinessPreflightAfterHealthyMutation in runner-command-traits.ts:56, a separate "healthy enough to skip the preflight" axis.
No runner state.RunnerSession (runner-session-types.ts) has no state field, only booleans (ready, computed alive, lastHealthyMutation); the readiness-preflight decision in runner-session.ts:87–101 and the cache decision in runner-cache.ts:450–462 are string-literal unions that stand in for one.
29 timeout/budget constants across 12 files (runner-startup-transport.ts:36–40, runner-disposal.ts:26–33, runner-session.ts:78–81, runner-lease.ts:23–25, runner-device-set.ts:18–20, runner-sequence.ts:24–35, and single constants in six more files).
None of this is a bug. It is where the next incident will take longest to diagnose, and it is the code a contributor has to read to touch anything about runner startup.
Task
Three bounded cuts, each its own PR, in this order. Do not attempt a rewrite.
One response decoder. Make parseRunnerResponse (runner-session.ts) the only place a runner response body is decoded; have the recovery status probe and the adoption uptime probe call it (or a narrower function it exports) and delete the two private decoders. Tests: runner-command-recovery and runner-adoption suites keep their current expectations; add one test per probe proving a malformed body is rejected the same way the main path rejects it.
A session state enum. Add state: 'starting' | 'ready' | 'draining' | 'stopped' (adjust names to what the code actually distinguishes; derive from the existing booleans and the readiness-preflight reasons, do not invent states) to RunnerSession, with one transition function. Replace the six "alive" checks with two: process liveness (OS fact, host.ts) and session state. hasLiveIosRunnerSession becomes a state read. The readiness-preflight decision keeps its reason codes (they are diagnostics), but the decision reads state plus lastHealthyMutation. Any SessionState-like field added to a persisted record follows the R7/R10 rule in CONTEXT.md (owner entry plus schema bump).
Retire postCommandViaSimulator if it is dead. Establish first whether the curl-through-simctl path ever answers where the host TCP and usbmux paths do not, after usbmux became primary (Explore usbmux as the primary physical iOS runner transport #1403). Instrument with a diagnostic (ios_runner_startup_transport phase, which transport answered) and read it over the iOS simulator lanes and the nightly (.github/workflows/xctest-nightly.yml) for a week, or find the commit that introduced it and the failure it worked around. If no run needs it, delete it together with its encode path; if one does, document the condition next to the function and close this sub-task.
Acceptance criteria
After (1): grep -rn 'JSON.parse' packages/platform-apple/src/runner/*.ts (production files) shows exactly one site; pnpm check:affected --run green.
After (2): RunnerSession has a state field; grep -rn 'alive' packages/platform-apple/src/runner/*.ts shows only the process-liveness primitive and reads of state; the daemon device status output for a running iOS session is unchanged (compare JSON against main); the iOS simulator integration lane and the macOS host lane green; one recorded startup, one idle-stop and one recycle each transition through the enum, asserted in runner-session tests.
After (3): either the function and its encode path are gone and the startup transport tests are updated, or a comment above it names the concrete condition it serves with the run that proved it.
The layering check (pnpm check:layering) and pnpm check:production-exports stay green; no new exports from packages/platform-apple/src/runner/index.ts.
Non-goals
Merging or renaming files for their own sake. Changing lease, cache or xctestrun-artifact logic (runner-lease.ts, runner-cache*.ts, runner-artifact*.ts): those are a different cluster with their own invariants (#2598). Reducing the number of timeout constants.
Why
The largest maintenance surface on the Apple platform is not geometry, it is the daemon-side cluster that babysits the XCTest runner process:
packages/platform-apple/src/runner/is 38 production files, about 10k lines. A survey onmainat91652a8fc5found the following (file:line refer to that head):parseRunnerResponsePayloadinrunner-session.ts:973(the canonical one),parseLifecycleResponsePayloadinrunner-command-recovery.ts:353(thestatusrecovery probe), and an inlineJSON.parseinrunner-adoption.ts:140(theuptimeprobe).runner-transport.ts:48(sendRunnerCommandOnce, host TCP / usbmux),runner-usbmux.ts:52(raw HTTP framing over the usbmux socket),runner-startup-transport.ts:371(tryRunnerEndpoints, the startup connect probe) andrunner-startup-transport.ts:418(postCommandViaSimulator:simctl spawn <udid> /usr/bin/curl …, a shell-out inside the Simulator, still reachable from lines 119 and 400).isRunnerProcessAlive/isRunnerProcessTreeAlive/runnerSessionsStillAliveinrunner-disposal.ts:274/269/177;hasLiveIosRunnerSessioninrunner-client.ts:192readinggetRunnerSessionSnapshotinrunner-session.ts:442;probeRunnerAnswersUptimeinrunner-adoption.ts:127(wire-level); andcanSkipRunnerReadinessPreflightAfterHealthyMutationinrunner-command-traits.ts:56, a separate "healthy enough to skip the preflight" axis.RunnerSession(runner-session-types.ts) has nostatefield, only booleans (ready, computedalive,lastHealthyMutation); the readiness-preflight decision inrunner-session.ts:87–101and the cache decision inrunner-cache.ts:450–462are string-literal unions that stand in for one.runner-startup-transport.ts:36–40,runner-disposal.ts:26–33,runner-session.ts:78–81,runner-lease.ts:23–25,runner-device-set.ts:18–20,runner-sequence.ts:24–35, and single constants in six more files).None of this is a bug. It is where the next incident will take longest to diagnose, and it is the code a contributor has to read to touch anything about runner startup.
Task
Three bounded cuts, each its own PR, in this order. Do not attempt a rewrite.
parseRunnerResponse(runner-session.ts) the only place a runner response body is decoded; have the recoverystatusprobe and the adoptionuptimeprobe call it (or a narrower function it exports) and delete the two private decoders. Tests:runner-command-recoveryandrunner-adoptionsuites keep their current expectations; add one test per probe proving a malformed body is rejected the same way the main path rejects it.state: 'starting' | 'ready' | 'draining' | 'stopped'(adjust names to what the code actually distinguishes; derive from the existing booleans and the readiness-preflight reasons, do not invent states) toRunnerSession, with one transition function. Replace the six "alive" checks with two: process liveness (OS fact,host.ts) and session state.hasLiveIosRunnerSessionbecomes a state read. The readiness-preflight decision keeps its reason codes (they are diagnostics), but the decision readsstatepluslastHealthyMutation. AnySessionState-like field added to a persisted record follows the R7/R10 rule inCONTEXT.md(owner entry plus schema bump).postCommandViaSimulatorif it is dead. Establish first whether the curl-through-simctl path ever answers where the host TCP and usbmux paths do not, after usbmux became primary (Explore usbmux as the primary physical iOS runner transport #1403). Instrument with a diagnostic (ios_runner_startup_transportphase, which transport answered) and read it over the iOS simulator lanes and the nightly (.github/workflows/xctest-nightly.yml) for a week, or find the commit that introduced it and the failure it worked around. If no run needs it, delete it together with its encode path; if one does, document the condition next to the function and close this sub-task.Acceptance criteria
grep -rn 'JSON.parse' packages/platform-apple/src/runner/*.ts(production files) shows exactly one site;pnpm check:affected --rungreen.RunnerSessionhas astatefield;grep -rn 'alive' packages/platform-apple/src/runner/*.tsshows only the process-liveness primitive and reads ofstate; the daemondevice statusoutput for a running iOS session is unchanged (compare JSON againstmain); the iOS simulator integration lane and the macOS host lane green; one recorded startup, one idle-stop and one recycle each transition through the enum, asserted inrunner-sessiontests.pnpm check:layering) andpnpm check:production-exportsstay green; no new exports frompackages/platform-apple/src/runner/index.ts.Non-goals
Merging or renaming files for their own sake. Changing lease, cache or xctestrun-artifact logic (
runner-lease.ts,runner-cache*.ts,runner-artifact*.ts): those are a different cluster with their own invariants (#2598). Reducing the number of timeout constants.Related: #1403 (usbmux primary), #2324 / #2325 (startup budget), #2598 (process lock), ADR 0005 (runner interaction lifecycle), ADR 0019 (request-bound platform runtime).