Reference

Changelog

46 entries. The runner on npm, the VS Code extension and the JetBrains plugin, in one place.

  1. Runner

    • An Outline row no longer passes on another row's data in three more ways: a check naming a column that no step of the recording writes ("the <plan> plan shows ...") binds the recording to its row, a row's number is found where the page shows it grouped or with a decimal comma ("Up to $1,000" for 1000), and a request body's number is taken for a constant only where the body itself writes the column. A style check's property is a value a row can hold. A check run in the session a column names (As "<who>") follows that column; one run in another session (As "admin") does not, and a step writing the column counts only when it comes before the check in the same session. A row's number is also found negative ("−$1,000") and grouped by an apostrophe or a no-break space ("CHF 1'000").
    • An amount check fails when the target shows another currency than the captured amounts, and captures in two currencies are refused. A symbol is compared with the codes it can stand for: "$5" may be USD 5 or CAD 5, never NOK 5 or EUR 5, "€" is EUR, and a dollar its letters name is that dollar ("US$", "CA$", "A$", "R$" and others); every capture is compared with every other. A lowercase word that is also a code ("Top 10", "150 eur") is compared with codes only. A URL check reads a path whole past -, ., _, ~ and +, and past : before a letter ("/checkout" is not "/checkout-failed" or "/checkout_failed"; a host before its port, "localhost:3000", still matches), and a word starts as it ends: after a letter and a separator it is part of another ("paid" is not in "/orders/not-paid"; in a query "+" and a space still separate words, so "shoes" is in "?q=red+shoes"), a number also after a digit ("42" is not in "/1-42"), while an id-prefixed slug keeps its words ("blue-shirt" is in "/products/42-blue-shirt") and "example.com" is in "shop.example.com". A doubled separator ("/checkout--failed") is one, in replay and in specs saffron eject writes. A committed check of a path the app shows with a slug after it ("/products/42" on "/products/42-blue-shirt"), or of one word inside a slug ("shirt" or "US" in "/en-US/cart"), fails after upgrading: record it again with the whole slug, or check the id another way. A currency code is read beside the number, capitals first, so "Top up NOK 150" is in NOK, and "NET 150 EUR", "150 eur" and "EUR (150.00)" are in euros. Text inside <noscript>, or inside an SVG definition never drawn where it stands, is not shown text, whether the check's target is the container or that element itself.
    • API tests: a group's same check compares values exactly (two int64 payment references a digit apart differ, as do 0.1 and 0.10000000000000001, and a number and a string of its digits), a number in a body that JSON cannot hold as written is refused, in any spelling (9007199254740993, 9007199254740993.0, 9.007199254740993e15, 0.10000000000000001; it would be sent rounded to another value: send it as a string). A row or capture that held such a number and passed before is refused after upgrading, a differs check whose row value is empty is refused, an expected decimal with more digits than a number holds is refused, and a group's contract check ties the recording to the contract, so api pin re-records it.
    • A provider's daily quota, "exhausted your capacity" and "hit your limit" stop the run's AI sessions as other usage limits do. A key a vendor quotes with its middle starred or elided (sk-...XwVu) is masked, every Cursor error masks CURSOR_API_KEY, including one the SDK throws, and an Antigravity error that ends in JSON names its message. A verify scenario path that leaves the project through a link is refused.
    • Specs saffron eject writes open web pages only (never data:, file: or javascript:) and upload only the project's own files (no dotfiles, nothing outside the project, no link to a dotfile or out of the project, no hard link, no folders), as replay does; they refuse a differs whose value a blank row or capture empties, and a body number they cannot send exactly. An Examples cell holding such a number marks its test fixme instead of stopping the eject.
    • A """ or a triple backtick at the start of a description line no longer opens a doc string, which left every StepSet line after it unread: as in Gherkin, a doc string opens only after a step in a scenario, Background, StepSet or Fixture, never in a Feature, Rule or Examples description, in the runner and the language server alike.
    • An API that answers with a refused connection in its body or a field (its own database's) is a failure, never "not run, environment missing": only a request that got no response, or the browser's own navigation error, says nothing answered. The dashboard's recent failures pass over reds the app did not cause when telling whether a scenario still fails and for how long.
    • Recording: a credential an API handed out during the session is masked in everything the browser tools show the agent (a token in a page URL was shown whole, and an earlier value of a credential that changed); a page the agent opens with an {env:...} value in its URL must be on the app's origin or one the scenario writes, a URL with no scheme read as the browser opens it; code a check runs through browser_evaluate that clicks, submits, sets a value, navigates or sends a writing request is refused, as a click in a check is; with a browser already running (transport.browser or a transport.module), recordings run one at a time, as two sessions on one browser closed each other's pages.
    • A password or user name in a base URL is masked wherever a URL quotes it (https://qa:***@staging.test): the report's errors, the run history, proposals, status, diff, bundles, traces and the API transcript, and the agent is never shown it. Only there: the same word elsewhere in a message is left as it is.
    • A local notation beside the number (100 kr, Rs. 500, 5,00 zł) is a currency of its own in an amount check, so captures in "kr" fail a total shown in euros. A URL check reads a percent-encoded letter beyond a separator as a letter ("/checkout" is not in "/checkout-%C3%A9chou%C3%A9").
    • A proposal or recording the run cannot read (no permission, a folder in its place) is listed as unreadable instead of stopping status, diff, accept --all and the run's closing summary. The editor flags a {data:...} reference only where the runner reads it (a step, its table or doc string), never in a title, a description or a comment.
    • A Then recorded again under the same words that now expects another value ("Pending" where it checked "Paid", a status 500 where it expected 201, another JSON value or URL), or reads another element named the same way (test id "status" become "badge"), is held for review as a weakened check (a row or data token holding the same value is the same): a stale re-record no longer replaces a check that just failed. A heal that changed nothing is red, never a proposal, also with --no-verify. The usage an agent session spent stays counted when its proof replay cannot start.
    • An Outline row's short value (a word under 5 characters with no digit, such as "dev") is written as its <column> only where it stands alone: inside "devices panel" it stays as recorded, so another row no longer checks a text never on the page. A secret Playwright cut short with "…" in a trace line, or a captured credential cut short in a message, is masked.
    • Replay: a "not shown" check looks inside the app's own iframes too (its origin, or one it writes, shown on the page), as a "shown" check does, so an error drawn in one no longer passes both; another origin's frame (a chat widget's "Close") and a hidden one (a silent sign-in renewal) are not searched. A "not shown", count, "differs" or "URL does not contain" check, and a response check, passes only once the requests the last action started have answered and the passing state has held for half a second: a list still growing, a placeholder drawn during a refresh or a row gone for a moment no longer passes. A long-poll the page opened earlier is not waited for. A response check fails when the very request it matched (its URL and method, and the body or JSON it checks) was answered again with another status (a POST answered 200 and then 500); a refetch, a child path or a GraphQL query on the same endpoint is another request.
    • accept --step-set refuses a StepSet that ran in a named browser session (As "seller") and a proposal that fails an Examples row: filed for reuse, its steps would seed other sessions' or rows' steps. A Nordic price ("150,- kr", "150:- kr") keeps its currency. A base URL's Basic header an API echoes is masked; a URL in a query beside an email is not taken for a password; step text accept writes into the feature file keeps its URLs whole.
    • Recording: a capture's target is read as replay reads it before it is recorded, and a value it does not show is refused, so a later check never compares with another element; a status the step names exactly ("answer 404") is expected exactly, never as its class, and a status check names a status the step states; a step performed again drops its requests' responses too, so a later check never reads a dropped one; a response check is verified with the step's table values, as replay reads it.
    • Config: a key that names a known one misspelt (maxCostUSD, stict) is refused, naming the key meant, and any other unknown key is said; a capabilities value in quotes, an unknown snapshotMode and a hookTimeoutMs of 0 are refused; an empty SAFFRON_ENV keeps the config's env. status --json and the MCP project_status mask a password in any URL and a header value written whole. run --plan judges a pending proposal as the run does: a stale one is a recording, one filed on another machine is not replayed, a row-bound one is refused. The MCP verify tool says when the run did not finish cleanly (interrupted, scenarios not run, a failed hook).
    • A heal whose agent session could not start keeps the replay's failure, never "not run"; a fixture that failed in the app fails the scenarios behind it, never "environment missing"; the usage earlier agent sessions of a recording spent stays counted when a later one fails.
    • A password in the base URL, or a user name alone (https://TOKEN@host), is masked in the report, the run history and bundles. A run with --filter or of part of the features folder is partial in the history, so trends compare suite runs only. A red the app did not cause never makes a scenario flaky on the dashboard.
    • A CSV data file with a quote never closed is refused, naming the row, instead of reading every line after it as one field. The language server no longer stalls on a line holding a long run of spaces.
    • Replay lets the first candidate that finds the recorded element decide a check, shown or not: a hidden recorded element no longer passes a text, visible, value or enabled check on a fallback elsewhere, a list check (every {data:...}) no longer passes on a fallback or another frame listing more, and a watched message counts only on the element watched, never on a log line elsewhere. A currency-first value ($5.00) in a pattern no longer matches its negative (-$5.00).
    • A refused API request's reply to the agent masks a credential an API handed out in the URL, as the reply to a request that answered does.
    • Restoring a column over a secret no longer splices a short env value into a word (dev in /devices); a row value an API request's path holds URL-encoded is written back as its column, as in a browser URL.
    • A red the agent cannot fix (an unreadable recording, a row refused, the committed recording failing while another row's proposal waits) counts as a failure in the report and the run summary, never as "needs recording" with a command that would change nothing.
    • The HTML report sets the reds the app did not cause apart from failures: a stale recording in a run with --no-agent, a missing environment, an agent that could not start or a run over its budget. They get their own not run group under the failures (needs recording when that is the only reason), a ○ on the run map, and a failed count that says how many more did not run and why. Every scenario that needs recording names the others, with one saffron run command for their feature files. The run map counts them per feature file, and the recurring failure themes leave them out (run history now keeps the mark). They are still red, and the run still exits 1. Found in real-world use, where one edit to a shared StepSet made 45 of 48 reds stale recordings.
    • A request group's step seeds another scenario that sends the same requests with its own values: the same table header, request names and rows, each cell read through <row:column>, and so does a group check that reads each response's own row. Before, a group step was never seeded, so every scenario recorded it with the agent.
    • A StepSet accepted on its own (accept --step-set) after a scenario's recording was made seeds the steps that come from that StepSet over the scenario's older recording, with no seeded from another recording: note; the same words written straight in a scenario keep its own.
    • saffron status --json lists the StepSets each proposal recorded (stepSets), for an editor to offer accept --step-set.
    • saffron eject exports a count check as Playwright's count of shown elements, expect(locator.filter({ visible: true })).toHaveCount(n), the number as replay reads it (010 is 10).
    • A pattern check that fails where the pattern writes an optional part right after a value says the value is matched whole and how to write the part instead.
    • A currency code after a number counts as a price in a locator's name only after two digits or more: "Section 4 SEK" is no price.

    Editors (VS Code extension 0.8.1, JetBrains plugin 0.8.1)

    • Both editors offer saffron export (the review bundle), a draft pull request with the pending proposals (export --branch, after asking for a branch and confirming), saffron eject (after asking, for the open file or every one) and accept --step-set for one ticked proposal's StepSet, from the Proposals view's menu (VS Code) or toolbar (JetBrains).