fix(ios): prepare shared synthesized input without contacts - #2362
fix(ios): prepare shared synthesized input without contacts#2362thiagobrez wants to merge 5 commits into
Conversation
XCTest attaches its HID digitizer lazily, on the first synthesized event a runner process posts. On a cold or loaded simulator that attach can lag several seconds behind the synthesizeWithError call that triggers it. When the first synthesized gesture of a process is timed (a drag with an activation hold, a paced pan), the touch-down then lands seconds into a window whose later samples were scheduled relative to the intended touch-down, so the app reconstructs a malformed gesture even though synthesizeWithError reported success. Once attached, the digitizer stays attached for the runner process, so every later gesture — including after a target relaunch — lands on schedule. Force the one-time attach with a throwaway synthesized contact before the first real gesture. The gesture's own timings are unchanged; the warm-up only moves the unavoidable one-time attach cost off the first user gesture, and is a no-op on a warm host. Runs once per runner process, before the first `gesture` command. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Size Report
Startup median (7 runs, lower is better):
|
CI validation (draft run)All code-quality gates pass: Lint & Format, Typecheck & Package, Repo Guards, Coverage, Compatibility & Provenance, CodeQL, Bundle Size, and the iOS/macOS/Linux smoke lanes. The iOS lane exercised the change end to end on a simulator, all green:
On this warm CI simulator the one-time attach is cheap (433 ms), so the warm-up is a near-no-op — which is the intended behavior on a warm host. The cold-boot case it targets is not reproducible on an already-booted smoke simulator; that is what the external reproduction branch demonstrates the defect on, and what a maintainer cold-boot device run (this lane on The two red checks are pre-existing flakes unrelated to this iOS-only change and both re-run: Android Smoke ( |
|
Update: CI Integration passed on re-run (it was a flake). Android Smoke is a pre-existing repo-wide failure, not this PR: the same |
|
The warm-up at 5d4fd44 sends a real tap to the top of the app window. That point is not guaranteed to be inert in a status-bar-hidden or edge-to-edge app, so it can activate content before the requested gesture. Use a non-delivering warm-up and prove that it cannot change app state. It also runs only for gesture, while scroll and synthesized drag reach the same timed-input pipeline without it. Put the once-only preparation at the shared input boundary and test ordering across those routes. The reported warm CI run does not establish that the cold-start failure is fixed; a cold-start red/green run is still needed. |
|
Reviewed 5d4fd44. Agreeing with @thymikee on the two structural points, plus four things in the code itself. 1. The warm-up point is computed in one frame and delivered in another. 2. The warm-up adds AX and a screenshot ahead of the gesture it exists to speed up. 3. Non-delivering warm-up, concretely. The status-bar band is not inert even when the tap does reach the status bar: a status-bar tap triggers 4. Policy mismatch, and the flag is set before the attempt. The warm-up runs under On the shared boundary (@thymikee's second point) — the three synthesis entry points are On evidence: the three unit tests are Generated by Claude Code |
|
Addressed the structural feedback in 1cf7e5f:
I also restored Agent Device's iOS 26 pointer route in thiagobrez/react-native-reorderable#101, preserving the simplified candidate pipeline. Its workflows now accept an immutable upstream commit for source-build validation. Cold-start evidence is still in progress. The first local cold boot with this build passed (empty preparation took 5.2 s; all three drag results were prompt), but the local released baseline did not reproduce a silent drop. I have corrected the PR body to avoid claiming the causal diagnosis is established and am running the hosted baseline/source comparison before claiming resolution. |
|
The shared contact-free preparation at 444cfeb addresses the earlier structural findings, and the reported red/green bridge test covers ordering and retry. Coverage now fails because nightly skips this regression without running it separately. Add the isolated test invocation to nightly too, preserving the selection guarantees. Comparable cold-start baseline/candidate evidence is still pending, so this is not merge-ready yet. |
|
One correction to the original diagnosis from the local cold-run trace: the Recap digitizer is attached/detached per real gesture, rather than remaining attached for the whole runner process. In the revised run: There is no attachment during that empty preparation interval. This does not rule out preparation of other shared XCTest state, but it does rule out treating these logs as proof of the original persistent-digitizer explanation. Two valid local cold boots passed, but the source-baseline/fixed hosted comparison is still needed to establish whether this prevents the failure instead of just adding delay. Keeping the causal claim explicitly unproven. |
|
Addressed the nightly coverage gap in 211f232. Nightly now runs the preparation regression in its own test invocation, then the remaining suite. It merges both xcresult bundles before the existing source-derived executed-count assertion. The selection guard now unions separate invocations while respecting skips within each invocation; regressions cover removing the isolated run and conflicting flags in one command. No coverage invariant was waived. The 40 selection/summary tests and |
|
The nightly merge was verified locally: the isolated regression plus remaining suite merge to The first hosted candidate cold sample is not green: job 101549746436 has no usable first-gesture measurement. Boot 1 hit a 5 s toolchain-query timeout during prepare; boot 2 rejected both gestures before synthesis because viewport was 134 × 291.33 while selector points were in the 402 × 874 coordinate space; boot 3 failed selector resolution. Boot 2's post-relaunch gesture did eventually commit, with a 13.6 s post-command observation wait and a 10.45 s empty preparation. This does not establish the empty preparation solves cold input. The exact-source baseline and second candidate sample are still running. Downstream iOS 27 fallback passed, while the iOS 26/native contract lanes are still pending. I am examining the pre-synthesis blockers without folding them into the warm-up claim. |
|
Two further corrections from the recordings/logs:
Neither correction turns the hosted candidate red into green. The iOS 27 fallback and native downstream contracts passed; iOS 26 and the remaining cold runs are pending. |
|
The nightly selection fix looks good at 211f232. It runs the isolated regression and retains the full-suite count check; the reported local merged result contains all 226 passing tests. The remaining blocker is comparable cold-start evidence: the first hosted candidate sample does not establish that preparation fixes the original issue. Keep this draft until that comparison is usable. Android smoke is still red and iOS smoke is pending; the changed nightly workflow also needs a hosted run. |
|
The requested hosted nightly is running at 211f232: 34060558728. The iOS smoke retry has passed fixture compilation and the targeted XCTest regressions and is progressing through replays. Android main is also red (at a different smoke assertion); I started one unchanged Android rerun and am retaining the failures. All four downstream device lanes, consumer/runtime checks, and parity passed in 34056536337. iOS 26 used its existing one whole-job retry after the first attempt failed the pre-drag scenario wait. The optional final npm dry-run then failed on the already-published 1.0.0 version. A fresh complete matrix is running at the latest source SHA through the existing validation-only channel: 34060592647. Cold comparison is still not usable as a green claim. Exact base bd08e6 produced two unobserved first drops and one delayed observation. Candidate sample 2 had one prompt first observation, one delayed observation, and one falsely classified “lost”: its recording clearly shows the committed drop, while the observer failed with an invalid viewport. The harness now separates observer errors from established target absence and rejects failed alert setup. Fresh same-harness runs are underway: base, candidate. Keeping the PR draft as requested. |
|
The changed hosted nightly workflow passed at 211f232ee: isolated regression, remaining suite, merged results, and the unchanged source-derived count check ( Android attempt 2 repeats a pre-input selector failure. Its |
|
The Android smoke positioning correction is isolated in #2369, with required local checks passing at |
|
The separate Android smoke correction #2369 is now green at |
|
The corrected same-harness comparison finished; neither run passes validation. Downstream harness SHA is
Both candidate first-command rejections use viewport 134 × 291.33 with a selector point at 201,389. They occur before the preparation/real synthesis. Their recordings start with a black app surface even though setup's AX text checks succeeded. The later relaunches render the app. This is another readiness/coordinate-evidence problem, not evidence that the warm-up ran and lost those first two gestures. Candidate boot 1's post-relaunch gesture is also concerning: empty preparation completes at All three candidate preparation markers report success (~5.2–5.3 s). One clean candidate boot passes, but these data do not establish that empty preparation fixes the original symptom. Keeping this draft. I am tracing the viewport mismatch and the first actual synthesis after relaunch before another comparison. Is there an existing contact-free XCTest bootstrap that initializes the event-delivery machinery beyond an empty record? The current preparation does not attach a virtual digitizer in the retained traces. |
|
The retained simulator logs narrow this further: the app does receive UIKit touch events in the unobserved candidate boot-1 relaunch gesture. That contradicts a complete “no touch stream reached the app” explanation for this sample. Using timestamps from
These are logged UIKit dispatches, not a count of every hardware sample or an exact touch-down delivery measurement. The delayed first app dispatch and reduced dispatch count are consistent with queued/coalesced delivery disrupting the scripted 650 ms source hold and 1,200 ms movement; that causal link still needs touch-phase/timestamp evidence. In the candidate failure, BackBoard logs attachment at This strengthens the reason to keep the PR draft rather than interpreting its successful empty record as demonstrated readiness. |
|
The completed comparison does not establish the fix at 211f232. As reported, neither baseline nor candidate passed validation, and one candidate gesture still produced no observed drop after successful empty preparation. Keep this draft until a controlled reproduction demonstrates the intended improvement; successful preparation alone is not evidence of input readiness. |
Summary
Prepare a zero-pointer-path event record at the shared iOS synthesis boundary before constructing the first real timed input record. This adds no app-delivering contact, status-bar tap, frame lookup, accessibility query or screenshot.
Gesture/sequence, drag, controlled scroll, swipe and tap share preparation. Only success sets the process-wide flag; failure allows the real action and a later preparation attempt. Real input keeps its original timings. Six files change, including native regression isolation and nightly result merging/count enforcement.
Validation
At
211f232ee85df5742e3be093a008b46f80a6e1a7:pnpm check:affected --run, CI/coverage, and iOS smoke pass.[1,1,1,1,1,1]) and passes afterward ([0,1,0,1,1,1,1,1]), observing retry and one successful preparation across real mixed bridge routes.Draft: the hosted cold comparison and downstream matrix remain pending. Earlier hosted candidate samples were not usable green evidence. Per-gesture digitizer traces contradict the persistent-digitizer theory; the selector-press probe also uses private synthesis. Preparation's effect on the original symptom remains unproven. Android's independent pre-input smoke positioning failure has a green test-only correction in #2369.