* test: give the tap-retry differential a fixture control that forces the retry
tap-retry-if-no-change was parked in #1289 for being a coin flip: tapRetries
measured 0 in run 29504440599 and 1 in 29510020718 with no change to the flow or
commit. This re-adds it with a control that holds still, so the retry fires every
run.
The original diagnosis (a dynamic cart badge in the tapped title's subtree) had
the right shape but the wrong scope. maestroSnapshotSignature hashes EVERY node
on screen, not the tapped subtree, so no "static region" of the home screen could
have worked. The actual coin flip is the gesture lab's remote image
(reactnative.dev/img/logo-share.png): whether it lands before or after the tap
decides whether the engine sees "changed" and skips the retry.
So the fixture gets a dedicated inert surface — no state, effects, timers,
images, or pressables — presented as a full-screen modal so iOS detaches the
presenting screen and the tab bar's live badges leave the hierarchy too. On a
real run it is 9 nodes against Settings' 55.
Reached by a launcher on Settings via the Settings TAB, which is a deliberate
choice twice over. A deep link would have kept the launcher out of every other
screen's snapshot, but simctl openurl raises a SpringBoard "Open in app?"
confirmation on iOS 26 even cold, and Maestro's openLink goes through the same
path. And home's "Open settings" button sits below the fold, so reaching it needs
scrollUntilVisible — the engine bug already waived under #1299.
The flow carries no waitForAnimationToEnd: a navigating tap defers a stability
requirement that the next tap settles before resolving its target, so the
baseline signature is already captured on a settled screen. Adding one fails the
flow outright, which is a real engine divergence filed as #1326.
Verified on device (iPhone 17 Pro Max, iOS 26.2, Maestro 2.5.1): 10/10
consecutive differential runs ok, tapRetries [0,0,1] every run — the two
navigating taps correctly do not retry, the inert tap retries exactly once. A
single green run proves nothing here, which is the trap #1300 fell into.
The parking guard in invariants.test.ts is replaced by three guards: the scenario
stays active, carries its tapRetries invariant, and is never waived by a
knownDivergence — a flaky scenario must be fixed, not declared.
Fixes#1300
* chore: gitignore expo prebuild output in the test app
Building the fixture app locally (what .github/actions/setup-fixture-app does in
CI) runs expo prebuild and generates examples/test-app/ios/. It is generated and
untracked but not ignored, so it shows up in git status and a `git commit -a`
would sweep the whole native project in. Same for android/ when building there.
Scoped to examples/test-app, so the repo-root android/ — which holds real tracked
sources like android/ime-helper — is unaffected. Nothing is tracked under either
path today, and the CI fixture-app cache key hashes src/**, app/**, modules/**
and the config/lockfiles, so it does not reference these and is unaffected.