Skip to content

refactor(ios-runner): one response decoder, a session state enum, and a verdict on the curl-through-simctl transport #2662

Description

@thymikee

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 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.

  1. 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.
  2. 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).
  3. 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.
  • No timeout value changes in any of the three PRs (the constants are a smell, not a target; changing them changes startup timing, which Cold iPhone startup ignores the requested preparation deadline #2324/fix(ios): honor the startup budget through a cold Simulator boot #2325 calibrated).
  • 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.

Related: #1403 (usbmux primary), #2324 / #2325 (startup budget), #2598 (process lock), ADR 0005 (runner interaction lifecycle), ADR 0019 (request-bound platform runtime).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions