saffron-ai 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "saffron-ai",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Gherkin-native AI-fallback test runner: zero-token cached replay, runtime AI healing, honest reports",
5
5
  "scripts": {
6
6
  "test": "vitest run",
@@ -55,6 +55,7 @@
55
55
  "files": [
56
56
  "dist-pkg",
57
57
  "skills",
58
+ "textmate",
58
59
  "README.md",
59
60
  "LICENSE"
60
61
  ]
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: saffron
3
- description: Write, run, and maintain end-to-end UI tests with Saffron — a Gherkin-native runner where an AI agent records each scenario once and every later run replays at zero tokens. Use when creating or editing .feature / .saffron files, defining StepSets, reviewing .saffron/cache proposals, running `saffron run`, or when the user mentions Saffron, Gherkin scenarios, step sets, cached replay, or `saffron.config.json`.
3
+ description: Write, run, and maintain end-to-end UI tests with Saffron, a Gherkin-native runner where an AI agent records each scenario once and every later run replays at zero tokens. Use when creating or editing .feature / .saffron files, defining StepSets, reviewing .saffron/cache proposals, running `saffron run`, or when the user mentions Saffron, Gherkin scenarios, step sets, cached replay, or `saffron.config.json`.
4
4
  license: Saffron Free Use License v1.0 (see the saffron-ai package LICENSE)
5
5
  metadata:
6
6
  author: saffron-ai
@@ -12,14 +12,14 @@ metadata:
12
12
  Saffron runs plain-Gherkin scenarios in a real browser. There are **no
13
13
  step definitions and no glue code**: on the first run an AI agent performs
14
14
  each step and records what it did into a JSON cache; every later run
15
- replays that cache with plain Playwright — zero AI calls, zero tokens,
15
+ replays that cache with plain Playwright: zero AI calls, zero tokens,
16
16
  ~100 ms per scenario. When the UI drifts, the agent heals the failing
17
17
  step mid-run and files a reviewable proposal.
18
18
 
19
19
  Your job when writing tests is to make that economy work. Three rules
20
20
  matter more than everything else:
21
21
 
22
- ## 1. Exact step text is the cache identity — reuse the vocabulary
22
+ ## 1. Exact step text is the cache identity: reuse the vocabulary
23
23
 
24
24
  A step is looked up by its exact wording. `When I visit the login page`
25
25
  and `When I open the login page` are two different steps: the second one
@@ -37,25 +37,25 @@ npx saffron steps --json # machine-readable
37
37
  Prefer a `●` recorded wording verbatim. Quoted values may differ freely
38
38
  (`I enter "admin"` seeds from `I enter "bob"`), so parameterize with
39
39
  quotes rather than inventing new phrasings. `saffron init` registers the
40
- `saffron` MCP server with Claude Code, Cursor and VS Code — its
40
+ `saffron` MCP server with Claude Code, Cursor and VS Code; its
41
41
  `search_steps` / `list_step_sets` tools give the same answer without a
42
42
  shell.
43
43
 
44
- ## 2. `Then` steps are sacred — write them as the verdict
44
+ ## 2. `Then` steps are sacred: write them as the verdict
45
45
 
46
46
  Saffron never heals an assertion: the agent may help *reach* a `Then`,
47
47
  never make it pass. So `Then` lines must state exactly what must be true,
48
- with stable text — never volatile values (prices, dates, counters). Put
48
+ with stable text, never volatile values (prices, dates, counters). Put
49
49
  the final assertions **last**; the trailing block of `Then` steps is
50
50
  strict under every policy. Use `Given`/`When` for actions.
51
51
 
52
52
  ## 3. Never bake volatile or secret values into files
53
53
 
54
- - Secrets: `{env:VAR}` — `When I enter "{env:ADMIN_PASSWORD}" in the password field`.
55
- - Dates: write intent, not literals — "1 day from today" records as `{date+1}`.
56
- - Dynamic display values: capture and compare — `I record the total as "first"` … `"first" should differ from the displayed total`.
54
+ - Secrets: `{env:VAR}`, as in `When I enter "{env:ADMIN_PASSWORD}" in the password field`.
55
+ - Dates: write intent, not literals: "1 day from today" records as `{date+1}`.
56
+ - Dynamic display values: capture and compare, e.g. `I record the total as "first"` … `"first" should differ from the displayed total`.
57
57
 
58
- ## Writing a scenario — the shape that records cleanly
58
+ ## Writing a scenario: the shape that records cleanly
59
59
 
60
60
  ```gherkin
61
61
  Feature: Checkout
@@ -78,7 +78,7 @@ Guidelines the recorder rewards:
78
78
  - Name the page or control in the step (`on the checkout page`, `the "Save" button`).
79
79
  - Data belongs in tables and doc strings, not in prose (see references).
80
80
  - Conditional wording (`if prompted`) records as a no-op when absent.
81
- - Real-world furniture just works — say it: dialogs ("and confirm the
81
+ - Real-world furniture just works if you say it: dialogs ("and confirm the
82
82
  dialog"), uploads ("upload the file "x.txt" as the attachment"),
83
83
  drag-and-drop ("drag the card onto the done column"), iframes
84
84
  (interact normally), network waits ("wait for the order API to return
@@ -90,7 +90,7 @@ Guidelines the recorder rewards:
90
90
  changes, and you gain the `StepSet:` keyword.
91
91
 
92
92
  ```gherkin
93
- StepSet: Complete guest information # define — colon, like Scenario:
93
+ StepSet: Complete guest information # define: colon, like Scenario:
94
94
  Given I am on the guest information page
95
95
  When I enter guest name "Chathuranga"
96
96
  And I click the continue button
@@ -98,11 +98,11 @@ StepSet: Complete guest information # define — colon, like Scenario
98
98
 
99
99
  Scenario: Checkout happy path
100
100
  Given I am on the cart page
101
- StepSet Complete guest information # invoke — no colon, like a step
101
+ StepSet Complete guest information # invoke: no colon, like a step
102
102
  Then I am on the checkout page
103
103
  ```
104
104
 
105
- - Definition: `StepSet: <name>`. Invocation: `StepSet <name>` — the bare
105
+ - Definition: `StepSet: <name>`. Invocation: `StepSet <name>`, the bare
106
106
  name without the keyword is a silent parse error.
107
107
  - Names are project-wide unique; sets resolve across files; no nesting.
108
108
  - Application-wide flows (login, cookie banner) live in a sets-only
@@ -119,6 +119,7 @@ npx saffron run --no-agent # replay only (CI without AI access)
119
119
  npx saffron accept # list pending proposals
120
120
  npx saffron accept --all # promote verified proposals to caches
121
121
  npx saffron report # open the HTML report
122
+ npx saffron run --rerecord --filter @t # a recording is wrong: record it fresh
122
123
  ```
123
124
 
124
125
  Results: **green** = cached pass · **yellow** = passed with AI recording
@@ -142,11 +143,11 @@ re-record (mostly seeded from existing step recordings).
142
143
  | Put a password literal in a step | `{env:VAR}` |
143
144
  | Sleep/wait "for 3 seconds" | Wait for a UI signal or an API response |
144
145
  | One 30-step scenario | Several 5–10 step scenarios + StepSets |
145
- | Edit cache JSON to fix a test | Edit the scenario text; re-run |
146
+ | Edit cache JSON to fix a test | Edit the scenario text and re-run, or `saffron run --rerecord` if the recording itself is wrong |
146
147
 
147
148
  ## References
148
149
 
149
- - [Syntax cheat sheet](references/syntax.md) — tables, doc strings,
150
+ - [Syntax cheat sheet](references/syntax.md): tables, doc strings,
150
151
  outlines, secrets, dates, network waits, page furniture, StepSets.
151
- - [Configuration & CI](references/config.md) — `saffron.config.json`,
152
+ - [Configuration & CI](references/config.md): `saffron.config.json`,
152
153
  flags, `--strict`, cross-browser, workers, heal model, secrets setup.
@@ -39,12 +39,13 @@ your-project/
39
39
  |---|---|
40
40
  | `baseURL` | App under test; steps say "the login page", not full URLs |
41
41
  | `storageState` | Playwright storage-state JSON so replays and the agent start authenticated (`npx playwright open --save-storage=.auth/state.json <url>`) |
42
- | `strict` | Yellow (passed-with-adaptation) exits 1 until reviewed — cached-green-only CI |
42
+ | `retries` | Extra attempts per action with backoff before a step fails and the agent heals (default 1). Not a scenario re-run; reports show a `retried ×N` chip |
43
+ | `strict` | Yellow (passed-with-adaptation) exits 1 until reviewed: cached-green-only CI |
43
44
  | `assertionPolicy` | `strict` (default) or `adaptable-mid`; the final assertion block is always strict |
44
45
  | `verifyProposals` | Proof-replay every recording zero-AI before filing (default true) |
45
46
  | `reuseSteps` | Seed new recordings from existing step recordings (default true) |
46
47
  | `snapshotMode` | `none` (default, ~60% fewer AI calls) or `full` for highly dynamic pages |
47
- | `browser` | `chromium` / `firefox` / `webkit` — replay runs anywhere; recording and healing use Chromium |
48
+ | `browser` | `chromium` / `firefox` / `webkit`: replay runs anywhere; recording and healing use Chromium |
48
49
  | `workers` | Parallel replay workers; agent work stays sequential |
49
50
  | `healModel` | Cheaper model for heal sessions only |
50
51
 
@@ -68,7 +69,7 @@ preflight (e.g. a missing `{env:VAR}`).
68
69
 
69
70
  Recording and healing need Claude credentials: `ANTHROPIC_API_KEY`, or a
70
71
  Claude Code login on the machine. Replay-only runs (`--no-agent`) need
71
- none — that is the normal CI mode once caches are committed.
72
+ none: that is the normal CI mode once caches are committed.
72
73
 
73
74
  ## Recommended CI
74
75
 
@@ -83,8 +84,8 @@ credentials), review proposals in PRs like snapshot updates.
83
84
  ## Reading a report
84
85
 
85
86
  Per scenario: status, duration, AI calls and tokens (including prompt
86
- cache reads/writes — the real bill), adaptation narrative, cache diff and
87
+ cache reads/writes: the real bill), adaptation narrative, cache diff and
87
88
  suggested feature edit for yellows, drift chips when page fingerprints no
88
89
  longer match. Trends vs. the previous run and 20-run sparklines come from
89
90
  `.saffron/history.jsonl`; **chronic** scenarios (healing repeatedly) are
90
- flagged — re-record those instead of paying for heals again.
91
+ flagged: re-record those instead of paying for heals again.
@@ -18,7 +18,7 @@ trailing block of assertions is always strict; under the opt-in
18
18
 
19
19
  ## Scenario Outlines
20
20
 
21
- One cache serves every row — `<param>` placeholders stay in the cache and
21
+ One cache serves every row: `<param>` placeholders stay in the cache and
22
22
  resolve per row at replay.
23
23
 
24
24
  ```gherkin
@@ -71,7 +71,7 @@ When I leave a note
71
71
  Then the saved note should be shown
72
72
  ```
73
73
 
74
- ## Secrets — `{env:VAR}`
74
+ ## Secrets: `{env:VAR}`
75
75
 
76
76
  ```gherkin
77
77
  When I enter "{env:ADMIN_USER}" in the username field
@@ -81,7 +81,7 @@ And I enter "{env:ADMIN_PASSWORD}" in the password field
81
81
  Resolved from the environment (or a git-ignored `.env`; real env wins) at
82
82
  replay. Caches, proposals, reports and history contain only the token;
83
83
  a missing variable fails fast by name. Honest note: during the *first*
84
- recording the agent types the real value once — use rotatable staging
84
+ recording the agent types the real value once: use rotatable staging
85
85
  credentials.
86
86
 
87
87
  ## Dates and dynamic values
@@ -99,7 +99,7 @@ And the total should match "NOK [\d,]+"
99
99
  And the "Book" link should point to "/booking"
100
100
  ```
101
101
 
102
- ## Network-aware steps (no keywords — plain prose)
102
+ ## Network-aware steps (no keywords, plain prose)
103
103
 
104
104
  ```gherkin
105
105
  When I place the order and wait for the order API to return 201
@@ -152,7 +152,7 @@ Scenario: Checkout happy path
152
152
  - Names project-wide unique (also catches the colon typo at an
153
153
  invocation). Sets may contain tables, doc strings and `<param>`
154
154
  placeholders; no nesting; a set never runs standalone.
155
- - Shared flows go in `features/shared.steps.saffron` — a sets-only
155
+ - Shared flows go in `features/shared.steps.saffron`, a sets-only
156
156
  library file with a `Feature:` header and no scenarios.
157
157
  - Assertions inside a set are checkpoints mid-scenario and part of the
158
158
  strict final block when the set is invoked last.
@@ -0,0 +1,16 @@
1
+ {
2
+ "comments": {
3
+ "lineComment": "#"
4
+ },
5
+ "brackets": [
6
+ ["<", ">"]
7
+ ],
8
+ "autoClosingPairs": [
9
+ { "open": "\"", "close": "\"", "notIn": ["string"] },
10
+ { "open": "<", "close": ">" }
11
+ ],
12
+ "surroundingPairs": [
13
+ ["\"", "\""],
14
+ ["<", ">"]
15
+ ]
16
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "saffron-textmate",
3
+ "displayName": "Saffron (TextMate bundle)",
4
+ "description": "Syntax highlighting for .saffron files \u2014 Gherkin plus StepSet. Import this folder in JetBrains: Settings \u2192 Editor \u2192 TextMate Bundles \u2192 +. Shipped inside the saffron-ai npm package.",
5
+ "version": "0.1.0",
6
+ "publisher": "saffron-ai",
7
+ "engines": {
8
+ "vscode": "^1.75.0"
9
+ },
10
+ "contributes": {
11
+ "languages": [
12
+ {
13
+ "id": "saffron",
14
+ "aliases": [
15
+ "Saffron"
16
+ ],
17
+ "extensions": [
18
+ ".saffron"
19
+ ],
20
+ "configuration": "./language-configuration.json"
21
+ }
22
+ ],
23
+ "grammars": [
24
+ {
25
+ "language": "saffron",
26
+ "scopeName": "source.saffron",
27
+ "path": "./syntaxes/saffron.tmLanguage.json"
28
+ }
29
+ ]
30
+ }
31
+ }
@@ -0,0 +1,127 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
3
+ "name": "Saffron",
4
+ "scopeName": "source.saffron",
5
+ "patterns": [
6
+ {
7
+ "include": "#comment"
8
+ },
9
+ {
10
+ "include": "#tag"
11
+ },
12
+ {
13
+ "include": "#stepset-definition"
14
+ },
15
+ {
16
+ "include": "#block-keyword"
17
+ },
18
+ {
19
+ "include": "#stepset-invocation"
20
+ },
21
+ {
22
+ "include": "#step"
23
+ },
24
+ {
25
+ "include": "#docstring"
26
+ },
27
+ {
28
+ "include": "#table"
29
+ }
30
+ ],
31
+ "repository": {
32
+ "comment": {
33
+ "match": "^\\s*#.*$",
34
+ "name": "comment.line.number-sign.saffron"
35
+ },
36
+ "tag": {
37
+ "match": "@[^@\\s]+",
38
+ "name": "entity.name.tag.saffron"
39
+ },
40
+ "stepset-definition": {
41
+ "match": "^\\s*(StepSet)(:)\\s*(.*)$",
42
+ "captures": {
43
+ "1": {
44
+ "name": "keyword.control.stepset.saffron"
45
+ },
46
+ "2": {
47
+ "name": "punctuation.separator.saffron"
48
+ },
49
+ "3": {
50
+ "name": "entity.name.function.stepset.saffron"
51
+ }
52
+ }
53
+ },
54
+ "block-keyword": {
55
+ "match": "^\\s*(Feature|Background|Scenario Outline|Scenario Template|Scenario|Examples|Scenarios|Rule)(:)(.*)$",
56
+ "captures": {
57
+ "1": {
58
+ "name": "keyword.control.gherkin.saffron"
59
+ },
60
+ "2": {
61
+ "name": "punctuation.separator.saffron"
62
+ },
63
+ "3": {
64
+ "name": "entity.name.section.saffron"
65
+ }
66
+ }
67
+ },
68
+ "stepset-invocation": {
69
+ "match": "^\\s*(StepSet)\\s+([^:].*)$",
70
+ "captures": {
71
+ "1": {
72
+ "name": "keyword.control.stepset.saffron"
73
+ },
74
+ "2": {
75
+ "name": "entity.name.function.stepset.saffron"
76
+ }
77
+ }
78
+ },
79
+ "step": {
80
+ "match": "^\\s*(Given|When|Then|And|But|\\*)\\s(.*)$",
81
+ "captures": {
82
+ "1": {
83
+ "name": "keyword.other.step.saffron"
84
+ },
85
+ "2": {
86
+ "name": "string.unquoted.step-text.saffron",
87
+ "patterns": [
88
+ {
89
+ "match": "\"[^\"]*\"",
90
+ "name": "string.quoted.double.saffron"
91
+ },
92
+ {
93
+ "match": "<[^<>]+>",
94
+ "name": "variable.parameter.saffron"
95
+ }
96
+ ]
97
+ }
98
+ }
99
+ },
100
+ "docstring": {
101
+ "begin": "^\\s*\"\"\"",
102
+ "end": "^\\s*\"\"\"",
103
+ "name": "string.quoted.docstring.saffron"
104
+ },
105
+ "table": {
106
+ "match": "^\\s*\\|.*\\|\\s*$",
107
+ "name": "meta.table.saffron",
108
+ "captures": {
109
+ "0": {
110
+ "patterns": [
111
+ {
112
+ "match": "\\|",
113
+ "name": "punctuation.separator.table.saffron"
114
+ },
115
+ {
116
+ "match": "<[^<>]+>",
117
+ "name": "variable.parameter.saffron"
118
+ }
119
+ ]
120
+ }
121
+ }
122
+ }
123
+ },
124
+ "fileTypes": [
125
+ "saffron"
126
+ ]
127
+ }