Healing and re-recording
When the UI changes: healing
A cached step fails mid-run → bounded retries → the agent attaches to the same browser at the exact point of failure, diagnoses (drift vs defect), and either finishes the scenario (yellow + proposal) or fails it with an explanation (red). A heal is paid for once: until you accept it, the next run replays it at zero AI before calling the agent (§5). Then:
npx saffron accept # inspect the heal
npx saffron accept <file> --propagate # fix every cache using the same locator or request
npx saffron accept <file> --with-feature-edit # also rewrite the adapted steps in the feature file
--propagate: if the heal fixed a locator other scenarios also use (same role/name core), they're repaired in the same accept, one heal, N scenarios, zero extra tokens. The same holds for an API request: when the heal rewrote a request (a field the API now requires, a renamed path), every other cache sending that same request under the same step text, as scenarios sharing the step's wording do, takes the new one; accept prints the request as it was and as it is now. A request fix is not copied when it writes a value of the healed scenario's own (a table cell, a doc string, its Outline row), adds a<column>or{captured:...}value, drops a value the request extracted, sends the request to another API, or when the heal kept the request as it was in one place and rewrote it in another: accept says why, and those scenarios heal on their own run. Neither is a step whose heal added or removed an action. Pass--propagateon the accept itself: a plain accept names the other caches and leaves them, and the proposal is gone afterwards, so there is nothing left to propagate later.--with-feature-edit: applies the agent's corrected Gherkin to your feature file (an edit that would leave a step empty is refused when the proposal is read, and never applied) (Background steps are skipped: those you edit yourself) and keeps the cache in sync.saffron difflists these rewrites under "Feature file edits", each step as the proposal recorded it against its new text, so you see them before you apply them. A step from a step set is marked: accept rewrites it in the step set, for every scenario that uses it.
Read a heal's adaptations for the kind of change. The agent names it (a
locator or label change, a page removed or merged, a page added or
reordered) and the page it happened on. Notes starting flow change: come
from Saffron itself, not the agent: they list every click, key press, goto
or tab move the healed recording dropped or added against the committed
one, after a page at failure: note naming where replay stopped. For
example:
⚠ page at failure: replay stopped at step 4 ("I enter my guest details") on /checkout, where the heal started.
⚠ flow change: step 4 ("I enter my guest details") no longer performs 1 navigation action from the committed recording: click button "Continue to payment". A page or navigation step may have been removed or merged.
A flow change is worth a second look before accepting: confirm the app really changed its flow and did not break it.
Notes starting behavior change: are Saffron's too. They appear when a
healed step no longer enters, picks or uploads what the committed
recording did, and name what the step checks instead:
⚠ behavior change: step 3 ("I enter my card details") no longer enters fill label "CVC" with "123", and checks expect text "3 digits" to be visible instead. The scenario may now test something else: accept it only if it still exercises what the scenario is about, or edit the scenario.
Every heal is also checked against the requirement contract of the
recording it repairs: each value a step enters, each request it sends
(method and path) and each check it makes, read from the committed
recording. A repair may reach them through another element; it keeps a
requirement only when the same step still does that same thing. saffron diff and the review page (--html) list each requirement with the
repaired action that keeps it:
Requirement contract: 3 of 4 kept
kept step 2 enters "4242 4242": fill label "Card number" with "4242 4242"
NOT KEPT step 2 sends POST /payments
A request or check the repair no longer makes adds a requirement not kept: note, and accept --all leaves it out like a behavior change (an
input it no longer enters is that behavior change already). This shows
what stayed and the evidence for it; it does not prove the repaired
scenario means the same thing in general, so a click that reaches the page
another way is not a requirement, and the reviewer still reads the steps.
A heal serves the scenario's intent first, then the step's, and the step's
wording last. It may change how a step is done (another control, a merged
page, a missing click), never what the scenario exercises: typing card
details never becomes checking a hint, and a real payment or sign-in route
never becomes a simulator's button, even when the final Then still
passes. A value moved to another control (a text field that became a
dropdown) is still entered and raises no note. accept --all leaves a
proposal with a behavior change out; accept it by file once you have
confirmed the scenario still tests what it says. When the route a
scenario needs is missing from the environment (a product, a payment
method, an issuer redirect), the agent fails and names it instead of
working around it.
Notes starting insensitive check: come from the sensitivity check that
follows a passing proof. A proof shows a recording replays; it does not
show that its checks would notice anything. So Saffron replays the
scenario once more, with no AI, and runs each Then on the page as it was
just before the action step it follows. A check that passes there too sees
nothing that step did: the day picked was already picked, or the price
"changed" from a value it always had. The step it follows is the last one
that acted on the page: a step that only waits (for an element, a
response, a download) or only reads (a captured value, a watch armed for a
message) and an API request are passed over, and a
StepSet's last check, its exit guard, is not probed against the set's
own last action, as it says the whole set got where it should. A set of
checks only, or of waits and checks, is probed against the step before it
like any other check.
A "not shown" check in a Then that holds other checks is also run on its
own there. One whose element was not on the page before the step either
(a selector that matches nothing, say) holds whatever the page shows,
and gets the same note, naming that check: the step's other checks
failing before the step had hidden it. Check an element the step makes
disappear.
A "not shown" check naming a control by the start of its name (a textbox
Firstname, where the page shows Firstname Required) finds no element
before or after the step. Its note says so and names the control shown
before the step: name it in full.
⚠ insensitive check: step 3 ("the summary reads Mon") also passes before step 2 ("I choose Mon") runs, so it proves nothing about what that step does. Make the step change something the check reads, or the check read what the step changes; accept it by file only if that is intended.
Fix the scenario (pick a day that is not already picked, compare against a
value captured before the change), or accept it by file when the check is
meant to hold either way. accept --all leaves it out. A check that fails
before its step, or cannot run there, counts as sensitive, and the report
shows "checks proven sensitive in N of M" beside the proofs that passed.
The check runs only after a proof passed, so --no-verify skips it too.
A check of an HTTP response is not run this way: it reads the latest
response, and before its step that is another request's.
Notes starting recording: are also Saffron's own. They appear when the
agent did something outside the steps it was given: for example, clicks a
heal made before announcing the failed step (discarded: that is diagnosis,
not part of the fix), actions a seeded recording made before its first
step (filed under that step) or actions under a step that replay had
already passed (discarded; that step keeps its recording). A heal never
rewrites the steps before the one that failed.
Assertions never heal (default). If a Then fails as written, the run
is red and the report explains why. If the change is intentional, update
the feature file (or accept the suggested edit) and let the scenario
re-record. Projects that want checkpoint flexibility can opt into
--assertion-policy adaptable-mid: mid-scenario assertions may then be
adapted (with an adaptation note, a suggested feature-file edit, proof
replay, and your accept gate), but the final assertion block of every
scenario stays strict under any policy.
What the agent may change
Every change a proposal makes to a committed recording falls in one of
three classes. saffron diff marks each changed step with its class
(adaptation in --json, with a count of each in summary), and the
review page and the run report show it beside the step.
| Class | What it covers | What you do |
|---|---|---|
repair |
Only how a step finds the same element again or waits for it: a fallback selector added or changed (beside the role, name, label or test id that still name the element; the iframe it lives in names no element), a longer timeout, a wait for an element added or dropped. The step does the same actions on the same elements, in the same order, with the same values. | A quick look. The proof replay already showed the step works. |
review |
What a step does: an action added, removed or of another kind, a value typed or picked, a URL, a request, a response a step waits for and checks (its status, its body), an element named another way (a new name, label, test id, frame, or selector when that is all that names it: another name can be another element, "Pay with card" or "Pay with test simulator"), a step added or removed, and any change to a check (under adaptable-mid). A step one of Saffron's notes names (a behavior change, a volatile locator, an insensitive check) is review whatever changed. |
Read it: confirm the app changed, not broke. |
protected |
What the agent never changes: the scenario's wording, its Then steps under the default policy, the values the scenario, its Examples and data/ give. The agent can only suggest a wording change, as a feature edit. |
Apply a feature edit yourself, with accept --with-feature-edit. |
A first recording has no class: there is nothing it changed. The class
is worked out from the recordings themselves, never from the agent's
account, so an agent cannot call a change a repair. A renamed button is
review: the proof replay found a button by the new name, and only you
can tell it is the same one.
[review] I click the login button
- click button "Login"
+ click button "Sign in"
name: "Login" → "Sign in"
1 to review (what a step does changed).
When a recording is wrong
Caches are never hand-edited; the honest fix is saffron run --rerecord
on the affected scenarios, which discards their caches and records them
fresh (seeded from every other recording) into a reviewable proposal.
Two guards keep bad recordings out of the tree: a fill recorded with an
empty value on a step that passes one marks the proposal UNVERIFIED with
the reason, and saffron accept --all skips UNVERIFIED proposals unless
told otherwise. The recorder also understands every way the agent can
type (fill, pressSequentially, type) and drops the clearing
fill('') that precedes typing into the same field.