fix(ios): honor the startup budget through a cold Simulator boot
Summary
Closes #2324.
Supersedes the original implementation by @PrinceD96 (head 8bdb85b7a3ea749ad9c631a8acb3fe573c831124), who found the bug, the stale close --shutdown shortcut, and the cold-Simulator validation recipe; the commit carries a Co-authored-by trailer for them.
A never-booted iOS Simulator runs Apple's first-boot data migration, which can take several minutes. The boot wait in ensureAppleReady was capped at a fixed 120 seconds that neither prepare --timeout nor open could reach, so the cap preempted the caller's budget.
This head replaces the earlier implementation with the smallest change that makes the budget reach the boot wait:
- The boot wait takes an absolute deadline.
simctl bootandsimctl bootstatus -bshare it; expiry fails witherror.details.reason: boot_timeoutand a hint. The Simulator is left booting (a deadline is not a cancellation), so a retry finds it further along or ready. Ctrl+C keeps its existing shutdown-on-cancel behaviour. prepare --timeoutis one budget for the boot and the runner. The runner preparation gets what the boot left instead of the full budget again.open --timeoutis new and bounds the boot for a fresh session. The device claim is held fromopenonward, so a competing workspace getsDEVICE_IN_USEthroughout the cold start without any extra claim mechanism.- Client envelope.
openandprepareuse the existingbudget + 30s margin, never below the base envelopearithmetic (envelope: 'margin', the same shapewaitalready uses), so the daemon's structured timeout wins the race against the client-side daemon reset.prepare --timeout 240000now yields a 270s envelope instead of 240s. close --shutdownalways asks simctl. The session device carriesbooted: falsefrom selection time for its whole life, so the old shortcut reported a successful shutdown on a Simulator that stayed Booted. Reproduced live before the fix, verified fixed after.
Dropped from the earlier head, on purpose: the daemon-side timer that cancelled the whole request (the budget is scoped to the boot, so --foreground's snapshot is never cut by it), the typed cleanup-failure error and claim abandonment path, forcing runner readiness inside open, the waitForSimulatorBoot on an already-Booted device, and the MAX_STARTUP_TIMEOUT_MS plumbing. Timer overflow for absurd --timeout values is pre-existing for every --timeout command and is out of scope here.
Validation
Unit: readiness (deadline shared by boot + bootstatus, expiry ā boot_timeout, no shutdown; default 120s kept), lifecycle (prepare shares the budget; open forwards its deadline), timeout policy (envelope arithmetic + reviewed sets), open CLI/structured input, shutdown runtime. pnpm check:affected --run green.
Live, local Xcode 26 / iOS 26.2 runtime, three freshly created never-booted iPhone 17 Simulators, isolated --state-dir:
open com.apple.Preferences --timeout 5000on a never-booted Simulator: failed after 8s total withboot_timeout+ hint; request log showssimctl boot1.8s thenbootstatuscut at 3.1s (one shared budget). Simulator left Booted.open com.apple.Preferences --timeout 600000on a never-booted Simulator: success in 77s (boot 4s, bootstatus 52s). This Mac finishes the first boot under the old cap, so the unit test carries the cap-removal proof; the live run proves the budget propagates and the flow completes.prepare ios-runner --timeout 600000on a never-booted Simulator: success in 104s (bootstatus 40s, then runner connect + health check).close --shutdownafter opening a Simulator selected while Shutdown: before the fix it returnedshutdown.success: truewith empty stdout/stderr and the Simulator stayed Booted; after the fix simctl is called and the Simulator reaches Shutdown.