Skip to content

fix(ios): wait for hidden-keyboard synthesized text to commit before responding - #1676

Merged
thymikee merged 4 commits into
mainfrom
claude/nervous-goodall-4953a1
Aug 7, 2026
Merged

fix(ios): wait for hidden-keyboard synthesized text to commit before responding#1676
thymikee merged 4 commits into
mainfrom
claude/nervous-goodall-4953a1

Conversation

@thymikee

@thymikee thymikee commented Aug 7, 2026

Copy link
Copy Markdown
Member

Consolidates the hidden-keyboard text-entry flake into one PR. It started as a test-side fix; that approach was withdrawn — see below.

The defect

The synthesized-first-responder bare-type route returned ok as soon as the private XCTest event record was posted, while the target app was still committing characters. Agents calling type then snapshot could observe a truncated field, and the smoke test observed it as a flake: ("h") and ("hardware-ke") instead of ("hardware-keyboard") on 3 of 5 runs of a branch with zero Swift changes (example job).

The fix polls the tapped element after dispatch until the value commits, then responds. Originated in #1673 (commit c9575ad, authored there); folded here after that PR was closed.

What changed relative to #1673

The P1 on #1673 is addressed by removing the repair path, not by defending it. A 600 ms quiet prefix cannot distinguish delayed commit from a dropped suffix — re-synthesizing the difference could post it while the original was still queued and commit the text twice, after the command had already reported ok. So the wait is now observation only:

  • deleted synthesizedTextCommitRepairTail, the quiet-window constant, the stall tracking, and its unit test;
  • deleted the synthesizer parameter — with no repair, the wait has no reason to hold a TextEntrySynthesizing at all, so it is a pure observer of the field it was given.

What remains is one bounded loop with a single decision point: poll until the value commits, the app transforms it (formatter, mid-caret, autocomplete), the value becomes unreadable (secure field, element stopped resolving), or the 3s ceiling expires. Every stop reason funnels through the pure synthesizedTextCommitProgress policy function, which is unit-tested without a simulator.

Known limit, unchanged by this PR: a genuinely dropped suffix still reports ok. Bare type is no-verification by design (repairMode == .none); this only removes the false truncation caused by commit lag, at a worst case of 3s.

The withdrawn test-side fix

This branch first made the test poll textField.value until it settled. That was wrong to ship on its own: the flake was a true positive about the product, and a polling assertion would have made the test tolerate a runner that responds before the text commits — deleting the guard that proves this fix works. It is reverted here (commits caecdc6d5 + f91c0e8b1 kept for the trail). The one-shot read stays as the assertion, because with the commit wait in place the value is committed by the time type responds.

CI gap found while folding

ios.yml enumerates its runner tests by hand with 25 -only-testing: flags, so #1673's new unit tests would never have executed in CI. testSynthesizedTextCommitProgressWalksExpectedPrefixOnly is now wired into that list.

Verification

Built with AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS=1, run on a dedicated iPhone 17 Pro Max sim (iOS 26.2) to keep a parallel session's simulator work out of the results:

  • 4 tests × -test-iterations 312/12 passed, twice (before and after the simplification).
  • testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden: 12.66–12.75s with the commit wait vs 12.5–13.7s without — it exits as soon as the value commits, so it costs nothing in the normal case.

Local machines cannot reproduce the CI truncation even under saturation, so this is non-regression evidence plus the mechanism; the 5/5-under-load A/B that showed the wait absorbing ~390ms of post-dispatch lag is in #1673.

…r test

testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden read textField.value
in one shot right after executeTypeCommand returned. The simulator commits
synthesized keystrokes after the command responds, so on a loaded CI machine
the read landed mid-word — observed failures reported ("h") and
("hardware-ke"). It failed on 3 of 5 runs of a branch carrying zero Swift
changes and passed on re-run.

Poll the value until it holds the expected text (10s ceiling) and assert on
the last value read, so a real regression still fails with what the field
actually held. Both reads in the test use the same helper; the assertions are
unchanged.
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 1.99 MB 1.99 MB 0 B
JS gzip 642.6 kB 642.6 kB 0 B
npm tarball 770.6 kB 771.4 kB +804 B
npm unpacked 2.70 MB 2.70 MB +2.8 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.6 ms 28.0 ms +0.4 ms
CLI --help 65.1 ms 65.7 ms +0.6 ms

Top changed chunks: no changes in the largest emitted chunks.

@thymikee

thymikee commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

[P1] This is not complementary to #1673; it removes #1673’s strongest production-route oracle. The contract under repair is that the synthesized-first-responder type response cannot outrun the field commit. Polling for up to 10 seconds after executeTypeCommand returns makes the exact false-success behavior green again: production can return on a partial value and this test will pass once the delayed suffix arrives. Merge/fix #1673 first and keep an immediate post-response assertion proving the full value is already committed. If that assertion still flakes, #1673 has not established its claim and must be corrected/evidenced instead of teaching the integration test to tolerate an early response. The cited flakes are real, but they are evidence of the production timing bug, not evidence that the oracle should be weakened.

…responding

The synthesized-first-responder bare-type route returned ok as soon as the
private XCTest event record was posted, while the target app was still
committing characters. On slow CI simulators the trailing characters landed
after the response, so agents (and the smoke test) observed a truncated
field value through the public type path.

After dispatch, poll the tapped element until its value reaches
textBefore + typedText, exit immediately when the app transforms the input
(formatter, mid-text caret, autocomplete), and if progress stalls as a
strict prefix, re-synthesize the missing tail once. Submit-suffixed text
keeps the old immediate return so a repair can never double-submit.

Validated on a booted iPhone 17 Pro simulator: 5/5 passes of
testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden under full-core CPU
load, with the commit wait absorbing up to ~390ms of post-dispatch lag that
the previous code ignored (uniform ~494ms dispatch-only before); the tail
repair never had to fire, consistent with commit lag rather than true drops.
Addresses the P1 on #1673: a 600 ms quiet prefix cannot distinguish delayed
app-side commit from a genuinely dropped suffix, so re-synthesizing the tail
could post it while the original was still queued and commit the text twice
after the command had already reported ok.

Drop the repair path entirely — the tail builder, the quiet-window constant,
the stall tracking, and the synthesizer dependency the wait only needed in
order to repair. What remains is a bounded observation: poll the tapped
element until the value commits, the app transforms it, the value becomes
unreadable, or the 3s ceiling expires. A dropped suffix still reports ok,
exactly as before this change; only the false truncation from commit lag is
removed.

The submit-key skip stays, for its own reason: the app may clear or rewrite
the field on submit, so textBefore + typedText is not the value to wait for.

Also wire testSynthesizedTextCommitProgressWalksExpectedPrefixOnly into the
iOS smoke lane — that workflow enumerates its tests by hand with
-only-testing, so a new test that is not listed never runs.
@thymikee thymikee changed the title fix(test): wait for typed text to settle in the hidden-keyboard runner test fix(ios): wait for hidden-keyboard synthesized text to commit before responding Aug 7, 2026
@thymikee

thymikee commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Clean review on head 54c0979b2: the synthesized commit wait is observation-only, so the unsafe suffix re-synthesis is gone; the immediate post-response oracle remains intact; and the targeted runner policy test is now included in the hand-enumerated iOS lane. Exact-head simulator evidence is adequate. Code review clean; ready for human.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Aug 7, 2026
@thymikee
thymikee merged commit 5f90d90 into main Aug 7, 2026
27 checks passed
@thymikee
thymikee deleted the claude/nervous-goodall-4953a1 branch August 7, 2026 15:34
@github-actions

github-actions Bot commented Aug 7, 2026

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