feat(flow): record the screen identity gate from a fingerprint read - #729
Closed
hubgan wants to merge 3 commits into
Closed
feat(flow): record the screen identity gate from a fingerprint read#729hubgan wants to merge 3 commits into
hubgan wants to merge 3 commits into
Conversation
… 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
force-pushed
the
feat/flow-screen-and-idle-conditions
branch
from
August 4, 2026 07:23
a71b63d to
88670cc
Compare
hubgan
force-pushed
the
feat/recorder-screen-identity-gate
branch
from
August 4, 2026 07:23
02b9e6e to
3715422
Compare
hubgan
force-pushed
the
feat/flow-screen-and-idle-conditions
branch
from
August 5, 2026 08:36
88670cc to
e1d9a9a
Compare
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
A
screen-fingerprintcall made throughflow-add-stepis rewritten into theawait: { 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-tap→tap:andrestart-app→launch: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
launchstep, 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 whoselaunchdeclares 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.
delayMsis dropped loudlyA
screengate has no pre-step delay because it polls. Every other rewrite refuses to convert whiledelayMsis 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 namestimeout:as the field that expresses the same intent.