Skip to content

Layer-3 differential: tap-retry scenario needs a fixture control that truly forces retryIfNoChange #1300

Description

@thymikee

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions