Skip to content

refactor(ios): brand the simctl builders and require the brand where runXcrun runs simctl - #2917

Merged
thymikee merged 5 commits into
mainfrom
refactor/2878-branded-simctl-builders
Sep 24, 2026
Merged

thymikee merged 5 commits into
mainfrom
refactor/2878-branded-simctl-builders

Conversation

@thymikee

@thymikee thymikee commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Summary

Follow-up to #2898 (addresses its review); refs #2878.

25 files touched, +410/-260.

Validation

Tested at 607e6b8078 (review follow-up on top of 064fa5ff3d).

  • pnpm check:affected --run at 607e6b8078: EXIT 1, all failures 5 s timeouts under host
    contention (other sessions' suites running). Format, lint, typecheck, layering (R79 tests pass,
    zero violations), di-seams, fallow, mcp-metadata, build, package, integration-node and
    macos-coverage pass. Every failed file passes when rerun alone except
    interaction-response-shape.contract.test.ts "press @ref", which times out identically on
    origin/main. The remaining light gates pass alone: integration-progress, replay-compat,
    daemon-wire-compat, affected-selector, gate-manifest(-model), depgraph, mutation-model.
  • Changed-line coverage (platform-apple + touched src/ tests under --coverage,
    pnpm check:coverage-changed --base origin/main): 24/24 lines, 6/6 branches.
  • The 064fa5ff3d Coverage failure was the Android snapshot-helper-session "cancels a
    stalled snapshot" 5 s timeout; it passes 3/3 alone on this branch and on origin/main.
  • pnpm check:production-exports: EXIT 0, 68 unused exports at warn (unchanged
    count), 18 suppressed, 0 stale suppressions; none of the 25 touched files appear.
  • Live: a scoped-set run (throwaway iPhone 17 Pro / iOS 26.2 sim) confirms the
    simctl --set <set> launch <udid> … argv still carries the set correctly, then
    fails with Invalid device because the runner's xcodebuild set-redirect
    (runner-device-set.ts) empties the scoped set during a cold build-for-testing.
    An A/B against origin/main reproduces the identical failure, so this is a
    pre-existing runner issue, not introduced by this branch or Enforce simulator-set scope for simctl: device scope for any udid, set scope only for calls that name no device #2878.
  • Remaining risk: that runner set-redirect issue is unaddressed, outside this diff.

buildSimctlArgsForAddress and buildSimctlArgsForDevice now return a
ScopedSimctlCommand, minted in core/tool-provider.ts from set-scoped
arguments. runXcrun takes that command or an argv whose tool is a named
non-simctl tool (devicectl, xcdevice, xctrace), so a literal, const-held,
string-typed or copied simctl argv is a type error. The simctl branch
unwraps the command to its scoped arguments; the ScopedSimctlArgs double
cast is gone.

The Apple tool host hands a simctl request's scoped args straight to the
simctl provider. The set-scope primitive is private to core/simctl.ts;
doctor and inventory use the named mints simctlAvailabilityProbeArgs and
simctlListDevicesArgs. Every argv stays byte-identical.

Refs #2878
Drop the '--set' literal ban, the global ArrayExpression clause, the
set-scope identifier check and the tool-provider argv exemption. R79 now
checks the argv at an xcrun invocation (a call with an 'xcrun' argument
or an executable: 'xcrun' spec), which must name its tool as a literal
other than simctl, and refuses casts to the scope brands outside the two
modules that mint them.

Refs #2878
…ver it reaches an executor

runCmd, runCmdBackground and an executable: 'xcrun' spec take any string
argv, so the runXcrun brand does not reach them. R79 again flags every
array whose first element names simctl (literal, quasi-only template, or
a same-file binding of either) outside core/simctl.ts and
core/tool-provider.ts, so a variable-held, aliased or spread simctl argv
fails the gate as the inline form does.
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.80 MB 4.80 MB +34 B
Package (unpacked) 4.80 MB 4.80 MB +34 B
Package (download) 1.44 MB 1.44 MB +23 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 22.9 ms 23.3 ms +0.4 ms
CLI --help 62.5 ms 64.3 ms +1.8 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 064fa5f. The brand looks right: every simctl route I traced goes through the mint points, and the argv output does not change. I read the @ts-expect-error proofs and the R79 tests but did not run typecheck or the layering gate.

One design question before this is ready: could runXcrun drop simctl completely? Its two simctl callers (the app-launch console launch and the perf-target ps) could go through runSimctlForDevice or provider.simctl.run with ScopedSimctlArgs. Or does ScopedSimctlCommand still pay for itself because builder output also feeds runCmdBackground for the snapshot bridge, log stream, and screen recording?

Not blocking: the rule in apple-simulator-scope-policy.ts#L62 checks only the first array element, so a variable tool name or xcrun options before the tool could pass. Also, tool-provider.ts passes simctlCommand(args) directly at line 58 but spreads it into a copy at line 94 for the same executor shape; one form for both would be clearer.

The Coverage failure is the Android snapshot-helper-session "cancels a stalled snapshot" timeout, which is unrelated to this iOS-only diff; a rerun should clear it. Smoke Tests was still queued at review time.

@thymikee

Copy link
Copy Markdown
Member Author

Follow-up to the 064fa5f review, pushed as 607e6b8078 (no rebase: the PR is MERGEABLE and main's 6 new commits do not touch its files).

Can runXcrun drop simctl? Not cheaply. runXcrun has about 16 simctl callers, not two. They include runSimctlForDevice itself, simulator boot/bootstatus/shutdown/list, runner-session uninstall/bootstatus, runner-disposal terminate, runner-startup-transport curl, launch-diagnostics, app-settings biometrics, perf/perf-target, and platform-runtime-runtime-hints. ScopedSimctlCommand still pays for itself either way. Builder output also goes to plain executors: runCmdBackground for the snapshot bridge and screen recording, executable: 'xcrun' for log stream, and execFailureDetails for error details. So the brand has to stay. Removing simctl from runXcrun would be a separate step: route the runXcrun(buildSimctlArgsForDevice(...)) callers through runSimctlForDevice, and make runSimctlForDevice call provider.simctl.run(scopeSimctlArgsForDevice(...)). That is a mechanical change across about 12 files, and I left it out of this PR.

R79 checks only the first element. Partly fixed in 607e6b8078. An argv whose first element is an xcrun option (['--sdk', 'iphonesimulator', 'simctl', …]) now counts as a simctl argv when simctl appears anywhere in it. This applies to both inline and variable-held arrays. Legitimate option-first calls (['--find', name], ['--sdk', sdk, '--show-sdk-version']) still pass, and new tests cover both cases. A variable tool name is already refused inline at an xcrun call (the existing [tool, 'spawn', …] HAND_BUILT case). A variable tool name inside an array held elsewhere needs dataflow, which a syntactic rule cannot do. tsc still rejects that shape at runXcrun.

tool-provider.ts 58 vs 94. Fixed in 574ecbc5e5. Both now call one runSimctlCommand(runCommand, args, options), which passes a copy of the frozen command, because AppleToolCommandExecutor is SDK-supplied and typed string[].

Coverage. Your diagnosis is correct: the Android snapshot-helper-session 5 s timeout. It passes 3/3 alone on this branch and on origin/main. The new push starts a fresh run. Changed-line coverage measured locally is 24/24.

Validation: see the updated PR body. Local check:affected failed only on 5 s timeouts under host contention. Each failed file passes alone, except one contract test that times out the same way on origin/main.

@thymikee

Copy link
Copy Markdown
Member Author

This is ready at 607e6b8; the brand routing from the earlier review (064fa5f) still holds since this delta doesn't touch it. Not blocking: namesNonSimctlTool only checks the first argv element, so an option-first inline xcrun call such as ['--sdk', sdk, toolVar, 'spawn', udid] could pass the HAND_BUILT check at a plain executor even though the tool position isn't a literal — the rule should skip the leading xcrun-option literals and their values before checking the tool position, or refuse an option-first inline argv whose remaining elements include a non-literal tool, but this can be taken or left. Smoke Tests is still queued, not failed, and it's the check that exercises this simctl dispatch route (runXcrun -> provider.simctl.run -> runSimctlCommand); since the argv content is unchanged and only the array is now a copy, a failure there would point back at this PR, but none has occurred. I didn't run tsc, the layering gate, or the R79 tests, and relied on reading the rule and tracing the old code path; I also didn't re-trace every simctl call site outside this delta, since the prior review already covered brand routing there. Smoke Tests needs to finish green before this merges.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 24, 2026
@thymikee
thymikee merged commit cdfbad6 into main Sep 24, 2026
18 checks passed
@thymikee
thymikee deleted the refactor/2878-branded-simctl-builders branch September 24, 2026 16:46
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-24 16:47 UTC

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

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant