Skip to content

refactor(ios-runner): key TS runner retry rules on typed connect-failure reasons - #2820

Merged
thymikee merged 3 commits into
mainfrom
refactor/2787-ts-runner-typed-retry-reasons
Sep 23, 2026
Merged

thymikee merged 3 commits into
mainfrom
refactor/2787-ts-runner-typed-retry-reasons

Conversation

@thymikee

@thymikee thymikee commented Sep 23, 2026

Copy link
Copy Markdown
Member

Summary

Every runner connect failure agent-device builds now carries a typed details.runnerConnectFailureReason:

  • xcodebuild_exited_early
  • runner_connect_refused, set by both the connect deadline and the simctl spawn fallback
  • runner_endpoint_probe_exhausted

The three retry rules that matched our own message text now key on this field. The reasons are mutually exclusive, so the restart-before-send predicate no longer needs its connect-retry composition, and that is removed. 8 files changed. Closes #2787.

Text rules kept, now under foreignMessageIncludesAll, each commented with its source:

  • device_busy_connecting: Xcode/CoreDevice
  • fetch_failed: Node fetch (undici)
  • econnrefused: Node net
  • socket_hang_up: Node http

Where this departs from the issue:

  • The reason is not on details.reason, because the refused and early-exit failures already publish their BootFailureReason there.
  • Device-busy still matches the message. Moving it to stderr could end the connect loop early, which would change retry outcomes.
  • Test doubles for our failures now carry the typed reason. Their expected verdicts are unchanged.
  • The probe error never escapes waitForRunner.

Validation

  • pnpm check:affected --run passes at e670a921c: format, lint, typecheck, layering, fallow, build, and 2655 related tests.
  • Each converted rule has a test: the reason decides the verdict, whatever the message says. Producer tests cover the early exit, the connect deadline and the simctl spawn fallback.
  • Review: 2 findings fixed (stale docs, a test with an error shape no producer builds). 5 rejected as out of scope or having no consumer: the daemon, replay and boot-diagnostics text sniffs, a wrapper that inherits the reason, and embedder providers.
  • Not run on a device. Retry decisions are pure, so no device was needed.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 23, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.80 MB 4.80 MB +241 B
Package (unpacked) 4.80 MB 4.80 MB +241 B
Package (download) 1.43 MB 1.43 MB +76 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 30.0 ms 28.0 ms -1.9 ms
CLI --help 82.9 ms 83.5 ms +0.6 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at e670a92. The classifier change looks correct, and I did not find a code problem worth blocking on.

The Coverage job fails in scripts/tests/test-file-size-ratchet.test.ts, and the two files it names, runner-client.test.ts (1063->1067) and runner-command-retry.test.ts (1230->1250), are both touched by this diff, so the failure traces directly to this change. The Smoke Tests jobs were still queued when I looked, so I have no result from them, and the claim that runner behavior is unchanged rests on the classifier read, not a device run. I did not run the tests myself, so the claim that the new assertions fail against the pre-change rows is a reading of those rows, not an observed run. I also did not trace callers outside platform-apple for the wrapped error from prepareIosRunner (packages/platform-apple/src/runner/runner-lifecycle.ts#L228); if a daemon-level consumer classifies that error, it will now match runner_connect_refused where the old text match did not, and that path is worth a look before this ships.

To get the size ratchet green, shrink runner-client.test.ts by dropping the shouldRetryRunnerConnectError/isRetryableRunnerError cases at https://github.com/callstack/agent-device/blob/e670a92/packages/platform-apple/src/runner/__tests__/runner-client.test.ts#L419-L441, since runner-error-classification.test.ts already asserts those verdicts, and shrink runner-command-retry.test.ts by adding one local fixture const (for example const refused = () => runnerConnectFailure('runner_connect_refused', 'Runner did not accept connection')) so each call site fits on one line instead of three.

Not blocking: is the runner_endpoint_probe_exhausted row at https://github.com/callstack/agent-device/blob/e670a92/packages/platform-apple/src/runner/runner-error-classification.ts#L256 (and its tag at runner-startup-transport.ts#L300) reachable at all now that the PR says it never escapes waitForRunner, worth a follow-up to delete or document if not; and could take or leave: file a follow-up to move the remaining text matches in direct-ios-selector.ts#L103, session-test-infrastructure.ts#L8-9, and boot-diagnostics.ts#L75 onto details.runnerConnectFailureReason now that the typed field exists.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 23, 2026
thymikee and others added 2 commits September 23, 2026 17:53
… reasons

The runner connect path now publishes `details.runnerConnectFailureReason`
(`xcodebuild_exited_early`, `runner_connect_refused`,
`runner_endpoint_probe_exhausted`) where it builds each failure, and the
retry table keys those three rules on that field instead of our own
message text. The remaining text rules read only foreign text (Node's
fetch/net/http errors, Xcode's device-busy text) under a renamed
`foreignMessageIncludesAll` field, each naming its tool.

Test doubles that stood in for these failures now carry the typed reason,
and each converted rule has a test that the reason, not the message,
decides the verdicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… redundant

With the connect-failure reasons exclusive, no error that grants a restart
before send can also carry a terminal connect verdict, so the restart
predicate reads its own axis. Doc comments now say which failures carry a
BootFailureReason and that the rows above the startup block include
foreign-runtime text matchers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…owning file

runner-client.test.ts asserted shouldRetryRunnerConnectError and
isRetryableRunnerError, both exported by runner-error-classification.ts and
already covered there, growing it past the test-file size ratchet. Move the
one uncovered case (runner_endpoint_probe_exhausted keeps the connect loop
retrying) into runner-error-classification.test.ts and drop the rest as
duplicate message-sniff coverage.

Give runnerConnectFailure a reason-keyed default message so the retry
suite's repeated connect-refused/probe-exhausted fixtures fit on one line
again, bringing runner-command-retry.test.ts back to its merge-base length.
@thymikee
thymikee force-pushed the refactor/2787-ts-runner-typed-retry-reasons branch from b963726 to 8c20923 Compare September 23, 2026 15:55
@thymikee
thymikee merged commit 9b76f60 into main Sep 23, 2026
17 of 18 checks passed
@thymikee
thymikee deleted the refactor/2787-ts-runner-typed-retry-reasons branch September 23, 2026 16:11
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-23 16:13 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.

refactor(ios-runner): TS runner retry rules keyed on self-produced message text → typed reasons

1 participant