Skip to content

fix(ios): let a Simulator open wait out a slow app discovery before observing the launch - #2838

Merged
thymikee merged 1 commit into
mainfrom
fix/ios-first-wait-after-open
Sep 23, 2026
Merged

thymikee merged 1 commit into
mainfrom
fix/ios-first-wait-after-open

Conversation

@thymikee

@thymikee thymikee commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Summary

iOS Smoke smoke:automation-input step 7 (wait text "Agent Device Tester" 10000 right after open --relaunch) failed 15 times 09-14..23, 4 on main, always wait_capture_stalled with captures: 1. The CI request logs (runs 35314997169, 35462595081, 35589604091, 35643318229, 35869384788) show one poll: runner findText (2.6–3.9 s), then target discovery, then the first AX-bridge preparation (ios.snapshot-source.prepare 1.7 s, or bridge-preparation-pending), then the first bridge connection until the deadline cancelled it. The runner was already live; no restart.

Cause: the open's launch-observation probe treated simulator-target-discovery-pending (one 1.5 s slice) as an unobservable app. So the open returned before the discovery, bridge preparation and bridge connection, and the wait paid for all three.

Fix: the probe keeps joining the running discovery, bounded by the discovery's own 15 s deadline. Any other resolution failure still ends it at once. The pending predicate moves to snapshot-target.ts, which owns the reason. 7 files.

Validation

Tested at 84c47cca78: pnpm check:affected --run passed.

  • Regression: the new route test (an open waits out a slow app discovery…) and the observability test fail when the pending check is reverted to return undefined.
  • Live (iOS 26.2 sim, runner live on Settings, fresh daemon, fixture reinstalled): after the fix, 10/10 opens reported postOpenObservation: observable and 8/8 waits passed (63–475 ms). A never-matching wait's first poll took 0.46–0.52 s. Before the fix, 5/9 opens were unobservable (settle ≈ 1.8 s = the slice), and the first poll carried the bridge preparation and spawn (3.6 s). The full 10 s stall did not reproduce locally.

…bserving the launch

The launch-observation probe read a target discovery that outlasted one
1.5 s wait slice as an unobservable app. On a loaded host simctl
launchctl list takes longer than that, so open --relaunch returned
before the discovery, the AX-bridge preparation or the first bridge
connection, and the first wait after it paid all three in one poll
behind a runner findText. On iOS smoke CI that poll consumed the whole
10 s budget (wait_capture_stalled, captures: 1) at step 7 of
smoke:automation-input.

The probe now keeps joining the running discovery, bounded by the
discovery's own deadline; any other resolution failure still ends it at
once. The discovery-pending reason and its predicate move to the
resolver that owns them, shared by the capture route.
@github-actions

Copy link
Copy Markdown

Size Report

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

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.5 ms 28.8 ms +0.3 ms
CLI --help 83.1 ms 85.7 ms +2.5 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 84c47cc. The open now waits out a slow app discovery within its deadline, and a short --timeout still aborts through the shared signal. The code looks right, and your before/after Simulator run covers the changed path.

Not blocking: resolveLaunchedTarget (packages/platform-apple/src/snapshot-observability.ts#L107) is a second retry-on-pending loop over the same resolver as resolveTargetForObservation (packages/platform-apple/src/snapshot-route.ts#L236), with only a different exit rule. Could snapshot-target.ts hold one loop that takes the exit rule as a parameter?

iOS Smoke was still running at review time. It exercises this route (open --relaunch, then the first wait), so a failure in smoke:automation-input step 7 would likely be related. The known live-snapshot-depth-frontier.ts:126 failure is unrelated; #2832 fixes it.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 23, 2026
@thymikee
thymikee merged commit 3fac0d8 into main Sep 23, 2026
17 of 18 checks passed
@thymikee
thymikee deleted the fix/ios-first-wait-after-open branch September 23, 2026 17:17
@github-actions

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

@thymikee

Copy link
Copy Markdown
Member Author

Follow-up on the non-blocking note about resolveLaunchedTarget vs resolveTargetForObservation: tried it in #2851 and closed it. The two loops share only the pending check (about 4 lines). They differ in what they do with a non-pending error (return undefined vs rethrow) and in when to keep waiting (always vs only without a live runner). Sharing them needed a 5-parameter exported function and a second abort check, and grew the diff by 30 lines. Both loops stay local; each is under a dozen lines and covered by tests.

thymikee added a commit that referenced this pull request Sep 24, 2026
…readable

am start -W returns when the activity draws its first frame, which can be a
splash or an empty root while a React Native app still mounts. The first
capture after a cold open --relaunch then saw a content-poor tree and spent
its own budget on re-captures. The open now captures the launched app through
the interactor snapshot path, whose content verdict and bounded re-capture
decide readiness, and reports postOpenObservation. An app that stays
unreadable, or a failed capture, still opens as unobservable.

Refs #1571 (iOS half: #2838).
thymikee added a commit that referenced this pull request Sep 24, 2026
…readable

am start -W returns when the activity draws its first frame, which can be a
splash or an empty root while a React Native app still mounts. The first
capture after a cold open --relaunch then saw a content-poor tree and spent
its own budget on re-captures. The open now captures the launched app through
the interactor snapshot path, whose content verdict and bounded re-capture
decide readiness, and reports postOpenObservation. An app that stays
unreadable, or a failed capture, still opens as unobservable.

Refs #1571 (iOS half: #2838).
thymikee added a commit that referenced this pull request Sep 24, 2026
…readable

am start -W returns when the activity draws its first frame, which can be a
splash or an empty root while a React Native app still mounts. The first
capture after a cold open --relaunch then saw a content-poor tree and spent
its own budget on re-captures. The open now captures the launched app through
the interactor snapshot path, whose content verdict and bounded re-capture
decide readiness, and reports postOpenObservation. An app that stays
unreadable, or a failed capture, still opens as unobservable.

Refs #1571 (iOS half: #2838).
thymikee added a commit that referenced this pull request Sep 24, 2026
…readable

am start -W returns when the activity draws its first frame, which can be a
splash or an empty root while a React Native app still mounts. The first
capture after a cold open --relaunch then saw a content-poor tree and spent
its own budget on re-captures. The open now captures the launched app through
the interactor snapshot path, whose content verdict and bounded re-capture
decide readiness, and reports postOpenObservation. An app that stays
unreadable, or a failed capture, still opens as unobservable.

Refs #1571 (iOS half: #2838).
thymikee added a commit that referenced this pull request Sep 24, 2026
…readable (#2895)

* fix(android): return from an app open only after the launched app is readable

am start -W returns when the activity draws its first frame, which can be a
splash or an empty root while a React Native app still mounts. The first
capture after a cold open --relaunch then saw a content-poor tree and spent
its own budget on re-captures. The open now captures the launched app through
the interactor snapshot path, whose content verdict and bounded re-capture
decide readiness, and reports postOpenObservation. An app that stays
unreadable, or a failed capture, still opens as unobservable.

Refs #1571 (iOS half: #2838).

* fix(android): bound and type the launch observation of an app open

The open's launch capture now runs behind an injected launch observation
port with a fixed 6 s window of its own, separate from the caller's
cancellation. Only a content verdict, a system surface over the app, or
the window running out reads as unobservable. Any other capture failure
is a typed probe-failed result that the open reports and survives.

The capture is transient: it borrows a running helper session and stops
only a session it started, installs no helper, and does not retire the
helper after a content verdict. A URL open reports no observation, and
an app open whose package cannot be read reports app-unidentified.
PostOpenObservation is one documented union in the lifecycle contract,
shared with the Apple owner.

Refs #1571

* fix(android): let the launch settle window end re-captures, never helper work

The open's 6 s window reached the snapshot helper as an abort, so a
window that closed during a cold helper start or a borrowed capture tore
the helper down and left the next read to recover it. The window is now a
settle deadline on the transient capture: the content re-capture loop
starts no attempt after it, while helper start, capture and teardown keep
their own budgets and only the caller's signal cancels them.

The transient read replaces the injected port, the borrow session scope
and the interactor-side install mapping: the capture itself keeps a session
it found, releases one it started, installs no helper and does not retire
it after a content verdict.

Refs #1571

* fix(android): keep a missing-helper refusal out of capture failure recovery

A transient capture on a device without the current helper refused at the
install check, and that refusal went through the capture failure handler,
which logged an error and force-stopped the helper runtime on every new
device's first open. The refusal now reaches the caller directly.

The transient-capture tests move to their own file, so snapshot.test.ts
stays under the test-file size ratchet.

Refs #1571
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