Found by the layer-3 differential's own engine-side invariant in #1289 — a case of the oracle catching a defect in itself rather than in the engine.
What happened
tap-retry-if-no-change passed on both engines (maestro=pass, agent-device=pass) but its invariant failed:
invariant violated: highest tapOn tapRetries was 0 (< 1):
a tap on an unchanging screen must re-tap; zero retries means
retryIfNoChange never ran and the scenario proved nothing
Outcome parity was green, so without the invariant this scenario would have looked like it was guarding retryIfNoChange while never once exercising it — the exact vacuity the invariant exists to prevent.
Why the current control does not work
The flow taps the fixture app's title (Agent Device Tester) on the assumption that a non-interactive element cannot change the screen, forcing the no-change retry path. But tapRetries came back 0, so our snapshot signature did change between captures. Likely cause: the title is rendered by ScreenTitle with a dynamic badge={${props.cartCount} in cart} (HomeScreen.tsx:47-50), and the home screen carries other live content — enough for the hierarchy signature to differ, so the engine sees "changed" and never retries.
What is needed
A fixture control where a tap provably changes nothing:
- a genuinely static screen region (no dynamic badge/status/async content in the subtree), or
- a dedicated inert target added to the fixture app for this purpose, or
- assert on a different observable that proves the retry path ran.
Reproducing locally (do NOT iterate via CI)
pnpm maestro:conformance:differential -- --platform ios --only tap-retry-if-no-change --trace-root .agent-device
# then inspect resultTiming.tapRetries per step:
grep replay_action_stop .agent-device/**/replay-timing.ndjson
Evidence: https://github.com/callstack/agent-device/actions/runs/29504440599
Acceptance
The scenario is marked knownDivergence in differential/scenarios.ts with this issue as its tracking link. The fix removes the declaration; the runner then fails if the scenario passes while still declared, so this cannot be quietly forgotten.
Not an engine bug: retryIfNoChange itself is unit-tested and upstream-parity-checked (2-attempt cap). This is about the device scenario being able to observe it.
Found by the layer-3 differential's own engine-side invariant in #1289 — a case of the oracle catching a defect in itself rather than in the engine.
What happened
tap-retry-if-no-changepassed on both engines (maestro=pass, agent-device=pass) but its invariant failed:Outcome parity was green, so without the invariant this scenario would have looked like it was guarding
retryIfNoChangewhile never once exercising it — the exact vacuity the invariant exists to prevent.Why the current control does not work
The flow taps the fixture app's title (
Agent Device Tester) on the assumption that a non-interactive element cannot change the screen, forcing the no-change retry path. ButtapRetriescame back0, so our snapshot signature did change between captures. Likely cause: the title is rendered byScreenTitlewith a dynamicbadge={${props.cartCount} in cart}(HomeScreen.tsx:47-50), and the home screen carries other live content — enough for the hierarchy signature to differ, so the engine sees "changed" and never retries.What is needed
A fixture control where a tap provably changes nothing:
Reproducing locally (do NOT iterate via CI)
Evidence: https://github.com/callstack/agent-device/actions/runs/29504440599
Acceptance
The scenario is marked
knownDivergenceindifferential/scenarios.tswith this issue as itstrackinglink. The fix removes the declaration; the runner then fails if the scenario passes while still declared, so this cannot be quietly forgotten.Not an engine bug:
retryIfNoChangeitself is unit-tested and upstream-parity-checked (2-attempt cap). This is about the device scenario being able to observe it.