Skip to content

feat(flow): record the screen identity gate from a fingerprint read - #729

Closed
hubgan wants to merge 3 commits into
feat/flow-screen-and-idle-conditionsfrom
feat/recorder-screen-identity-gate
Closed

feat(flow): record the screen identity gate from a fingerprint read#729
hubgan wants to merge 3 commits into
feat/flow-screen-and-idle-conditionsfrom
feat/recorder-screen-identity-gate

Conversation

@hubgan

@hubgan hubgan commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #728.

What changes

A screen-fingerprint call made through flow-add-step is rewritten into the await: { screen: <route> } gate for the screen it just read.

A fingerprint read becomes a fingerprint check — recorded as the portable directive rather than a raw tool call, so the runner compares the route instead of merely re-reading it and discarding the answer. It is the same shape as the existing gesture-taptap: and restart-applaunch: rewrites.

Three cases the rewrite has to get right

A fingerprint that could not name the screen is refused. There is no identity to gate on. Recording it raw would persist a tool: step whose output nothing checks; the failure belongs where the evidence still is. The refusal distinguishes "this app can never have a reader" (gate on an element instead) from "no route at this instant" (let it settle and call again).

A fragment has no launch step, so at replay the gate would have no app to read the route from and would fail as an environment error — discovered long after the recording looked fine. The fingerprint call had to name the app to run at all, so that id is carried onto the step, with a warning that it pins the fragment to one platform and how to avoid that (compose it from an e2e flow whose launch declares the app).

A route equal to the flow's last gated route warns, but still records.

This one is a judgement call worth stating. Such a gate may prove nothing: an app whose sign-in form is presented inside its landing route reports one fingerprint for both screens, so a gate recorded after "tap Sign in" passes whether or not the tap landed.

But it may equally be a return trip — Home → Search → Home — where the gate is perfectly falsifiable. Telling the two apart needs the route as it was immediately before the last action, and the recorder does not have it: the last route it gated on says nothing about where the app went in between, since intermediate screens are routinely gated on an element instead — exactly what the skills prescribe for a route-less screen.

So it warns. A refusal needs certainty, and without it blocking would make the ordinary "go there, come back, verify" shape unrecordable — while the QA contract calls a navigation with no identity gate a blocking defect. The recorder would be making its own contract unsatisfiable.

delayMs is dropped loudly

A screen gate has no pre-step delay because it polls. Every other rewrite refuses to convert while delayMs is set, precisely so the delay survives; this one converts anyway — and dropping an author's parameter without a word is what must not happen. The warning names timeout: as the field that expresses the same intent.

hubgan added 3 commits August 4, 2026 09:22
… is this"

Every screen check argent had proxied identity through one element of the UI
tree, and an element is a weak proxy: a shared header matches on two screens, a
positional id encodes how many siblings exist, and during a push or modal
presentation source and destination are in the tree together — so a destination
landmark matches while the destination is still animating in. Worse, the
vocabulary of the tree is source-dependent: iOS `describe` prefers the
ax-service and falls back to native-devtools when the AX read comes back empty,
flipping labels to testIDs mid-session.

The focused React Navigation route path has none of that. It is read from the
app's own navigation state, so it is independent of the tree source, of content
(`/user/alice` and `/user/bob` share a route), and of locale. One route names
one screen.

It is read the black-box way argent already uses for the RN component tree —
walking the fiber tree via the devtools global hook and collecting focused
route/navigation prop pairs. No app cooperation is needed; the app does not have
to export its navigationRef.

Two limits are stated in the API rather than papered over, because callers must
pair around them: a native overlay ABOVE an RN screen (a permission alert, a
share sheet, a Modal) does not change the focused route, and navigation state
commits BEFORE the transition finishes animating. Route identity answers "which
screen", never "is anything covering it" and never "has it settled".

`available: false` is a distinct answer from `route: null`. The first says this
build can never have a reader (release build, fully native app, Chromium, Metro
down) and to recognize the screen by an element instead; the second says there
is no focused route at this instant and to probe again.

`metroServerRunning` is added alongside, and is deliberately not
`discoverMetro`: a Metro serving one app reports an EMPTY target list for
several seconds after that app relaunches, so "no targets" is the normal
post-launch state, not a down server. Answering the up/down question with target
discovery told authors to start a server that was already running.
Two checks a selector condition cannot express, which flows were substituting
fixed `wait:` steps for:

  await: { screen: "HomeTab>Profile" }   which screen, by exact route match
  await: { idle: true }                  has it stopped moving

They are deliberately separate, and a navigation wants both. A dropped tap
leaves the source screen perfectly idle, so readiness cannot prove identity;
navigation state commits before the transition animates, so identity cannot
prove readiness.

`idle` is `await-screen-idle`'s question asked against the tree the directives
actually resolve against — and unlike that tool it FAILS on timeout, which is
what makes it safe to persist in a flow. "Never stopped moving" and "never got a
readable tree" stay distinct: only the first is a verdict about the app.

The connect handling is most of this change, because caching it wrongly was
poisoning whole runs. `routeReaderFor` memoizes only SUCCESSFUL connects. The
debugger session is tied to the app process, so a `launch` invalidates it and
the gate that follows connects into the gap while the app re-registers with
Metro; remembering that failure made every later gate on the run fail instantly,
with no wait, which no per-step `timeout:` could rescue.

The first connect after a launch gets a much larger budget, because the app is
not slow to answer — it is not registered with Metro at all yet. Measured at
~12.5s for an RN app on a loaded simulator host, so the previous 5s cap made the
gate in the position the flow skills mandate (launch, then identity)
unreachable. That budget is spent only while Metro is actually reachable, so an
app with no Metro at all still fails fast.

Connecting is setup, so it is NOT charged against the step's polling budget: a
generous `timeout:` used to buy nothing on exactly the gate that needed it.

Every failure mode is distinct in the reason, because they call for opposite
responses. A DIFFERENT route means the app is genuinely elsewhere — the
regression a flow exists to catch. No route at all means the check could not
run, which is never allowed to read as a pass. A platform with no React
Navigation says so outright rather than advising a retry that cannot help.
A `screen-fingerprint` call made through the recorder is now rewritten into the
`await: { screen: <route> }` gate for the screen it just read — a fingerprint
READ becomes a fingerprint CHECK, recorded as the portable directive rather than
a raw tool call, so the runner compares the route instead of merely re-reading
it.

Three cases the rewrite has to get right:

A fingerprint that could not name the screen is refused. There is no identity to
gate on, and recording it raw would persist a `tool:` step whose output nothing
checks — the failure belongs where the evidence is.

A FRAGMENT has no `launch` step, so at replay the gate would have no app to read
the route from and would fail as an environment error, discovered long after the
recording looked fine. The fingerprint call had to name the app to run at all,
so that id is carried onto the step, with a warning that it pins the fragment to
one platform.

A route equal to the one the flow last gated on MAY prove nothing: an app whose
sign-in form is presented inside its landing route reports one fingerprint for
both screens, so a gate recorded after "tap Sign in" passes whether or not the
tap landed. But it may equally be a return trip — Home, Search, Home — where the
gate is perfectly falsifiable. Telling the two apart needs the route as it was
immediately before the last action, which the recorder does not have. So it
warns and records. A refusal needs certainty; without it, blocking would make
the ordinary "go there, come back, verify" shape unrecordable, and would make
the QA contract's "every navigation has an identity gate" unsatisfiable.

`delayMs` is dropped with a note rather than silently: a `screen` gate polls, so
a fixed pre-step sleep is the one thing it does not need — but dropping an
author's parameter without a word is what must not happen.
@hubgan

hubgan commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Closing with #727: this recorded the identity gate from a route fingerprint, and there is no fingerprint tool any more.

The recorder work here has no equivalent in the new shape, because the replacement identity gate is an ordinary await-ui-element on a destination-only element - already recordable, with no new command to teach flow-add-step. The readiness half, await: { idle: true }, is deliberately not recordable: it is a directive with no tool behind it, so it is added during polish alongside snapshot:.

#730 documents the pairing and now stacks directly on #728.

@hubgan hubgan closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant