Skip to content

fix(ios): diagnose a runner that cannot install, instead of blaming the screen - #1529

Merged
thymikee merged 3 commits into
mainfrom
fix/ios-provisioning-diagnosis
Jul 31, 2026
Merged

fix(ios): diagnose a runner that cannot install, instead of blaming the screen#1529
thymikee merged 3 commits into
mainfrom
fix/ios-provisioning-diagnosis

Conversation

@thymikee

@thymikee thymikee commented Jul 31, 2026

Copy link
Copy Markdown
Member

Second messaging fix from the #1521 hardware run, and the worse of the two: this one confidently states a cause that was never observed and recommends a remedy that cannot work.

What a user sees today

A physical iPhone that is not covered by the runner's provisioning profile cannot install the XCTest runner. open still succeeds — a CoreDevice app launch goes through devicectl and needs no runner — so the session looks healthy. Then every runner-backed command fails with:

iOS runner was already restarted during this request and "snapshot" still failed…
Hint: The current screen is overwhelming the iOS accessibility capture (usually heavy or animating content). The app session is preserved: run screenshot for visual truth…

Both halves are wrong here:

  • the screen was a stock Settings page; the runner had never started at all
  • screenshot is offered as the escape hatch, but on a physical device it needs the same runner, so it fails identically

The truth was sitting in the runner log the whole time:

Failed to install embedded profile for com.callstack.agentdevice.runner.uitests.xctrunner :
0xe8008012 (This provisioning profile cannot be installed on this device.)
** TEST EXECUTE FAILED **

I lost several minutes to "heavy Settings screen" before reading that log. Someone without log access loses far more, and no amount of retrying or screen-changing will ever help.

Change

Classify it. New IOS_RUNNER_DEVICE_NOT_PROVISIONED boot-failure reason, matched before the connect-timeout branch — a runner that cannot install also never accepts a connection, so the timeout reading would win and send people to look at the screen or the network instead of the signing account. The hint names the fix: register the device with the signing team, and notes that retrying will keep failing.

Deliberately not an infrastructure reason: infrastructure reasons imply a retryable environment blip, and no retry registers a device.

Stop asserting. The recycle-budget error only ever knows that a restart was already spent, never why. It now points at the runner log first and offers the heavy-screen reading second. That case is real — it just is not the only one, and it was being stated as fact.

A matching detail worth knowing

The real log came back partly in Polish (Nie można zainstalować…), because macOS localizes the installer prose. Matching therefore anchors only on the CoreDevice error code 0xe8008012 and the English framework strings (provisioning profile, embedded profile). The test uses the captured output verbatim, localized text included, so this stays honest.

Testing

  • classification asserted against the real xcodebuild output, verified revert-sensitive
  • a provisioning failure is asserted not to be infrastructure
  • a genuine connect timeout still classifies as IOS_RUNNER_CONNECT_TIMEOUT, so the new branch does not swallow it
  • 479 tests green across boot-diagnostics and the Apple core suites; lint and typecheck clean

The device that produced this has since been unpaired, so there is no live re-run — the evidence is the captured log, which the test asserts against directly.

check:affected shows provider-integration/daemon-entrypoint timeouts whose failing set differs on every run (7, then 3) with zero assertion failures, and all pass in isolation — the known contention signature on a host that has been driving devices for hours. Nothing in that set touches boot-diagnostics or the recycle ledger.

Follows #1527, which fixed the same class of misdiagnosis for Developer Mode and pairing.

Closes #1521 — its remaining item (two fully-live sessions) is blocked by a device that cannot join the signing account, not by an open engineering question. Full evidence for every other leg is in that issue.

…he screen

A physical device that is not covered by the runner's provisioning profile
fails to install the XCTest runner. Every runner-backed command then failed
with 'the current screen is overwhelming the iOS accessibility capture' and
advice to run screenshot instead — which fails identically, because it needs
the same runner. The suggested remedy could never work and the stated cause
was never observed.

Classify the install failure and say what it is: the profile does not cover
this device, register it with the signing team. It is deliberately not an
infrastructure reason, because no retry registers a device.

Matching anchors on the CoreDevice error code and the English framework
strings; the installer prose around them is localized by macOS, so the real
log arrived partly in Polish.

The recycle-budget error also stops asserting a cause it never observed. It
now points at the runner log first and offers the heavy-screen reading second,
which is where it belongs — that case is real, it just is not the only one.

Observed on an iPhone that could not be added to the signing account.
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.91 MB 1.91 MB +661 B
JS gzip 613.3 kB 613.5 kB +276 B
npm tarball 731.3 kB 731.5 kB +220 B
npm unpacked 2.57 MB 2.57 MB +661 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 22.3 ms 22.8 ms +0.5 ms
CLI --help 46.9 ms 46.3 ms -0.5 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/runner-client.js +103 B +49 B
dist/src/runner-disposal.js +69 B +26 B

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed exact head 4b01003. One blocking production-route issue remains:

P1 — the early-exit error still discards the new provisioning hint. buildRunnerEarlyExitError correctly computes IOS_RUNNER_DEVICE_NOT_PROVISIONED, but constructs details.hint through resolveRunnerEarlyExitHint, whose fallback always returns connect-timeout/cache-recovery guidance. For the captured xcodebuild failure, users are therefore still told to retry runner startup rather than register the device—the misdiagnosis this PR intends to fix. Thread the classified reason into the hint selection (preserving the busy-device special case) and add a regression test against buildRunnerEarlyExitError that asserts the reason/hint pair on the real captured output.

CI is green and the PR is mergeable, but this blocks readiness. The unavailable device prevents a repeat live run; the captured hardware log is acceptable residual evidence once the production route is covered.

buildRunnerEarlyExitError classified the provisioning failure correctly, then
built its hint through resolveRunnerEarlyExitHint, which ignored the reason and
always fell back to connect-timeout and cache-recovery guidance. The shipped
error therefore still told people to retry a runner that can never install,
which is the misdiagnosis the previous commit set out to remove.

Thread the reason through, keeping the busy-connecting device special case, and
withhold the cache-recovery sentence for a provisioning failure: clearing
derived data cannot put a device into a profile.

The previous commit only tested classifyBootFailure and bootFailureHint, never
the function that assembles the error a user receives. The regression added
here exercises that production route on the captured xcodebuild output.
@thymikee

Copy link
Copy Markdown
Member Author

Fixed at 08049c549. You were right, and the gap was in my own verification method rather than a detail I disagreed with.

buildRunnerEarlyExitError computed IOS_RUNNER_DEVICE_NOT_PROVISIONED correctly, then built details.hint through resolveRunnerEarlyExitHint, which took only the raw strings and ended in a hardcoded bootFailureHint('IOS_RUNNER_CONNECT_TIMEOUT'). So the classification landed in details.reason while the sentence a user reads still said retry runner startup — the exact misdiagnosis the PR claims to remove.

Why I missed it: the previous commit tested classifyBootFailure and bootFailureHint in isolation, never the function that assembles the error a user actually receives. Both unit tests passed and were revert-sensitive, and the production route was still broken.

Change: the classified reason is threaded into resolveRunnerEarlyExitHint, preserving the busy-connecting special case ahead of it. The cache-recovery sentence is also withheld for this reason specifically — clearing derived data cannot put a device into a provisioning profile, so appending it would only dilute an already actionable instruction.

Regression is against the production route, as you asked: buildRunnerEarlyExitError driven with the captured xcodebuild output, asserting the reason/hint pair together — that the hint names the profile and says to register the device, and that it does not say "Retry runner startup" or mention clean:xcuitest. Verified revert-sensitive: dropping the threaded argument fails it while the isolated unit tests stay green, which is precisely the hole you found.

Two guard tests came with it, so the fix cannot quietly swallow the other paths: an ordinary early exit still gets connect-timeout and cache-recovery guidance, and a busy connecting device keeps its own targeted hint.

482 tests green across the Apple core suites and boot-diagnostics; lint and typecheck clean.

@thymikee

Copy link
Copy Markdown
Member Author

Re-reviewed exact head 08049c549263960c3ff53248e3e0973712957e4d. The prior production-route finding is fixed: buildRunnerEarlyExitError now preserves the classified reason in the user-facing hint, and the new regression test exercises that assembled error. One blocking accuracy issue remains:

P1 — the new device-not-provisioned classifier matches generic signing failures. classifyBootFailure returns IOS_RUNNER_DEVICE_NOT_PROVISIONED when the log contains any of 0xe8008012, provisioning profile, or embedded profile. The latter two are broad xcodebuild vocabulary and can appear for expired profiles, missing signing certificates, invalid entitlements, or other profile-install failures where registering the device is not the remedy. That would reproduce the same confident-but-wrong DX this PR is fixing. Anchor this reason to the device-specific evidence—preferably 0xe8008012, or a sufficiently specific conjunction—and add negative cases proving unrelated provisioning/signing failures remain BOOT_COMMAND_FAILED.

CI is still running on this head, so no readiness label yet.

@thymikee

Copy link
Copy Markdown
Member Author

Addressed in 36c19be. The device-not-provisioned diagnosis now requires CoreDevice code 0xe8008012; generic provisioning/embedded-profile failures stay BOOT_COMMAND_FAILED instead of being mislabeled. Added regression cases for an expired profile and an invalid signing certificate, including the real early-exit connection wrapper.

Validation: pnpm check:affected --base origin/main --run passed (including 2,039 related tests, 5,217 coverage tests, and 141 provider-integration tests).

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Jul 31, 2026
@thymikee
thymikee merged commit d085946 into main Jul 31, 2026
30 checks passed
@thymikee
thymikee deleted the fix/ios-provisioning-diagnosis branch July 31, 2026 19:22
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-31 19:22 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.

Verify usbmux runner transport on multi-device and unpaired/untrusted hardware

1 participant