saffron-ai 0.3.0 → 0.4.0

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.3.0",
3
+ "version": "0.4.0",
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",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "files": [
56
56
  "dist-pkg",
57
+ "skills",
57
58
  "README.md",
58
59
  "LICENSE"
59
60
  ]
@@ -0,0 +1,152 @@
1
+ ---
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`.
4
+ license: Saffron Free Use License v1.0 (see the saffron-ai package LICENSE)
5
+ metadata:
6
+ author: saffron-ai
7
+ homepage: https://saffron-ai.lovable.app
8
+ ---
9
+
10
+ # Saffron
11
+
12
+ Saffron runs plain-Gherkin scenarios in a real browser. There are **no
13
+ step definitions and no glue code**: on the first run an AI agent performs
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,
16
+ ~100 ms per scenario. When the UI drifts, the agent heals the failing
17
+ step mid-run and files a reviewable proposal.
18
+
19
+ Your job when writing tests is to make that economy work. Three rules
20
+ matter more than everything else:
21
+
22
+ ## 1. Exact step text is the cache identity — reuse the vocabulary
23
+
24
+ A step is looked up by its exact wording. `When I visit the login page`
25
+ and `When I open the login page` are two different steps: the second one
26
+ costs a fresh AI recording (~$0.10–$2) even though the first already
27
+ replays for free.
28
+
29
+ **Before writing any step, look at what the project already knows:**
30
+
31
+ ```bash
32
+ npx saffron steps # every step: ● recorded, ● divergent, ○ unrecorded
33
+ npx saffron steps "login" # search
34
+ npx saffron steps --json # machine-readable
35
+ ```
36
+
37
+ Prefer a `●` recorded wording verbatim. Quoted values may differ freely
38
+ (`I enter "admin"` seeds from `I enter "bob"`), so parameterize with
39
+ quotes rather than inventing new phrasings. `saffron init` registers the
40
+ `saffron` MCP server with Claude Code, Cursor and VS Code — its
41
+ `search_steps` / `list_step_sets` tools give the same answer without a
42
+ shell.
43
+
44
+ ## 2. `Then` steps are sacred — write them as the verdict
45
+
46
+ Saffron never heals an assertion: the agent may help *reach* a `Then`,
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
49
+ the final assertions **last**; the trailing block of `Then` steps is
50
+ strict under every policy. Use `Given`/`When` for actions.
51
+
52
+ ## 3. Never bake volatile or secret values into files
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`.
57
+
58
+ ## Writing a scenario — the shape that records cleanly
59
+
60
+ ```gherkin
61
+ Feature: Checkout
62
+
63
+ Background:
64
+ Given I open the application
65
+ And I accept cookies if prompted # conditional steps are fine
66
+
67
+ @smoke
68
+ Scenario: Guest can complete checkout
69
+ When I add "Blue Widget" to the cart
70
+ And I place the order and wait for the order API to return 201
71
+ Then I should see "Order placed"
72
+ And the order request should have returned 201
73
+ ```
74
+
75
+ Guidelines the recorder rewards:
76
+
77
+ - One intent per step; 5–10 steps per scenario. Long scenarios heal badly.
78
+ - Name the page or control in the step (`on the checkout page`, `the "Save" button`).
79
+ - Data belongs in tables and doc strings, not in prose (see references).
80
+ - Conditional wording (`if prompted`) records as a no-op when absent.
81
+ - Real-world furniture just works — say it: dialogs ("and confirm the
82
+ dialog"), uploads ("upload the file "x.txt" as the attachment"),
83
+ drag-and-drop ("drag the card onto the done column"), iframes
84
+ (interact normally), network waits ("wait for the order API to return
85
+ 201", "wait until the job status API reports "READY"").
86
+
87
+ ## Repeated step groups → StepSets (`.saffron` files only)
88
+
89
+ `.saffron` is a superset of `.feature`: rename the file, nothing else
90
+ changes, and you gain the `StepSet:` keyword.
91
+
92
+ ```gherkin
93
+ StepSet: Complete guest information # define — colon, like Scenario:
94
+ Given I am on the guest information page
95
+ When I enter guest name "Chathuranga"
96
+ And I click the continue button
97
+ Then I am not on the guest information page
98
+
99
+ Scenario: Checkout happy path
100
+ Given I am on the cart page
101
+ StepSet Complete guest information # invoke — no colon, like a step
102
+ Then I am on the checkout page
103
+ ```
104
+
105
+ - Definition: `StepSet: <name>`. Invocation: `StepSet <name>` — the bare
106
+ name without the keyword is a silent parse error.
107
+ - Names are project-wide unique; sets resolve across files; no nesting.
108
+ - Application-wide flows (login, cookie banner) live in a sets-only
109
+ library file: `features/shared.steps.saffron` (still needs a
110
+ `Feature:` header; it yields no runnable scenarios).
111
+ - Start a set with a guard step, end it with an exit assertion.
112
+
113
+ ## The workflow
114
+
115
+ ```bash
116
+ npx saffron run # record misses, replay hits, heal failures
117
+ npx saffron run --filter @smoke # by tag
118
+ npx saffron run --no-agent # replay only (CI without AI access)
119
+ npx saffron accept # list pending proposals
120
+ npx saffron accept --all # promote verified proposals to caches
121
+ npx saffron report # open the HTML report
122
+ ```
123
+
124
+ Results: **green** = cached pass · **yellow** = passed with AI recording
125
+ or adaptation, a proposal awaits review · **red** = failed. Proposals
126
+ arrive stamped `verified ✓` (zero-AI proof replay passed) or
127
+ `UNVERIFIED ✗`; accept the former, investigate the latter. Commit
128
+ `.saffron/cache/` like snapshots; git-ignore `.saffron/reports/`.
129
+
130
+ **Never hand-edit files under `.saffron/cache/` or `.saffron/proposals/`**
131
+ unless deliberately authoring a manual cache (`"recordedBy": "manual"`).
132
+ To change behavior, change the `.feature`/`.saffron` text and let the run
133
+ re-record (mostly seeded from existing step recordings).
134
+
135
+ ## Anti-patterns that cost money or hide bugs
136
+
137
+ | Don't | Do |
138
+ |---|---|
139
+ | Invent a synonym for an existing step | Reuse the `●` wording from `saffron steps` |
140
+ | Assert inside a `When` ("When I see the dashboard") | Act in `When`, assert in `Then` |
141
+ | `Then the price is "NOK 1,148"` | `Then the price should match "NOK [\d,]+"` intent, or capture + compare |
142
+ | Put a password literal in a step | `{env:VAR}` |
143
+ | Sleep/wait "for 3 seconds" | Wait for a UI signal or an API response |
144
+ | 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
+
147
+ ## References
148
+
149
+ - [Syntax cheat sheet](references/syntax.md) — tables, doc strings,
150
+ outlines, secrets, dates, network waits, page furniture, StepSets.
151
+ - [Configuration & CI](references/config.md) — `saffron.config.json`,
152
+ flags, `--strict`, cross-browser, workers, heal model, secrets setup.
@@ -0,0 +1,90 @@
1
+ # Configuration, CLI and CI
2
+
3
+ ## Project layout
4
+
5
+ ```
6
+ your-project/
7
+ features/ .feature and .saffron files
8
+ saffron.config.json
9
+ .saffron/
10
+ cache/ committed replay caches → commit
11
+ proposals/ pending AI proposals → review, then gone
12
+ history.jsonl one line per run (trends) → commit recommended
13
+ reports/ latest.html / latest.json → git-ignore
14
+ ```
15
+
16
+ ## `saffron.config.json`
17
+
18
+ ```json
19
+ {
20
+ "baseURL": "https://stage.your-app.com",
21
+ "features": "features",
22
+ "actionTimeoutMs": 5000,
23
+ "retries": 1,
24
+ "model": "claude-sonnet-5",
25
+ "healModel": "claude-haiku-4-5",
26
+ "maxTurns": 100,
27
+ "storageState": ".auth/state.json",
28
+ "strict": false,
29
+ "assertionPolicy": "strict",
30
+ "verifyProposals": true,
31
+ "reuseSteps": true,
32
+ "snapshotMode": "none",
33
+ "browser": "chromium",
34
+ "workers": 1
35
+ }
36
+ ```
37
+
38
+ | Key | Meaning |
39
+ |---|---|
40
+ | `baseURL` | App under test; steps say "the login page", not full URLs |
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 |
43
+ | `assertionPolicy` | `strict` (default) or `adaptable-mid`; the final assertion block is always strict |
44
+ | `verifyProposals` | Proof-replay every recording zero-AI before filing (default true) |
45
+ | `reuseSteps` | Seed new recordings from existing step recordings (default true) |
46
+ | `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
+ | `workers` | Parallel replay workers; agent work stays sequential |
49
+ | `healModel` | Cheaper model for heal sessions only |
50
+
51
+ ## CLI
52
+
53
+ | Command | Purpose |
54
+ |---|---|
55
+ | `saffron run [paths] [--filter @tag] [--no-agent] [--strict] [--browser b] [--workers n] [--heal-model m] [--headed]` | Run; cached replay, agent on misses/failures |
56
+ | `saffron accept [file \| --all] [--with-feature-edit] [--propagate]` | Promote proposals; `--with-feature-edit` rewrites adapted steps in the feature file; `--propagate` applies a heal's locator fix to every cache using that locator |
57
+ | `saffron reject [file \| --all]` | Discard proposals |
58
+ | `saffron steps [search] [--json] [--snippets]` | The step vocabulary with recorded/divergent/unrecorded badges |
59
+ | `saffron author <prose-file>` | Draft a `.saffron` file from plain-paragraph requirements using the project vocabulary |
60
+ | `saffron mcp` / `saffron lsp` | Vocabulary for AI assistants / language server for editors |
61
+ | `saffron init` | Install this skill into the project's agent directories and scaffold config |
62
+ | `saffron report` | Open the latest HTML report |
63
+
64
+ Exit codes: 0 green/yellow, 1 red (or yellow with `--strict`), 2 usage /
65
+ preflight (e.g. a missing `{env:VAR}`).
66
+
67
+ ## AI access
68
+
69
+ Recording and healing need Claude credentials: `ANTHROPIC_API_KEY`, or a
70
+ 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
+
73
+ ## Recommended CI
74
+
75
+ ```bash
76
+ npx playwright install chromium
77
+ npx saffron run --no-agent --strict # replay committed caches; no AI, no surprises
78
+ ```
79
+
80
+ Record and heal on developer machines (or a dedicated job with
81
+ credentials), review proposals in PRs like snapshot updates.
82
+
83
+ ## Reading a report
84
+
85
+ Per scenario: status, duration, AI calls and tokens (including prompt
86
+ cache reads/writes — the real bill), adaptation narrative, cache diff and
87
+ suggested feature edit for yellows, drift chips when page fingerprints no
88
+ longer match. Trends vs. the previous run and 20-run sparklines come from
89
+ `.saffron/history.jsonl`; **chronic** scenarios (healing repeatedly) are
90
+ flagged — re-record those instead of paying for heals again.
@@ -0,0 +1,167 @@
1
+ # Saffron syntax cheat sheet
2
+
3
+ Everything valid Gherkin is valid Saffron. `.saffron` files add `StepSet:`.
4
+
5
+ ## Steps
6
+
7
+ ```gherkin
8
+ Given I am on the login page # action (healable)
9
+ When I enter "standard_user" in the username field
10
+ And I click the "Log in" button
11
+ Then I should see the products page # assertion (never healed)
12
+ But I should not see an error banner
13
+ ```
14
+
15
+ `And`/`But` inherit action-vs-assertion from the preceding step. The
16
+ trailing block of assertions is always strict; under the opt-in
17
+ `adaptable-mid` policy only mid-scenario checkpoints may be adapted.
18
+
19
+ ## Scenario Outlines
20
+
21
+ One cache serves every row — `<param>` placeholders stay in the cache and
22
+ resolve per row at replay.
23
+
24
+ ```gherkin
25
+ Scenario Outline: Failed login shows an error
26
+ When I enter "<username>" in the username field
27
+ And I enter "<password>" in the password field
28
+ And I click the "Log in" button
29
+ Then I should see the error message "<message>"
30
+
31
+ Examples:
32
+ | username | password | message |
33
+ | locked | secret | This user has been locked. |
34
+ | standard | wrong | Wrong username or password |
35
+ ```
36
+
37
+ ## Data tables
38
+
39
+ **2-column = key/value.** Values record as `<table:key>` references:
40
+ edit the values and replay stays free; change a key and the step
41
+ honestly re-records.
42
+
43
+ ```gherkin
44
+ When I enter the following credentials
45
+ | username | premium_user |
46
+ | password | secret2 |
47
+ ```
48
+
49
+ **Wider = records** (header row + one row per record). Cells record as
50
+ `<table:1:firstName>`, `<table:2:email>` (1-based rows). Editing any cell
51
+ replays free; adding/removing rows or renaming headers re-records.
52
+
53
+ ```gherkin
54
+ When I add the following guests
55
+ | firstName | email | city |
56
+ | Alice | alice@test.com | Oslo |
57
+ | Bob | bob@test.com | Bergen |
58
+ ```
59
+
60
+ ## Doc strings
61
+
62
+ Content records as `<docstring>`; rewording replays free. A later
63
+ assertion on that content follows the edit too ("the saved note should
64
+ be shown").
65
+
66
+ ```gherkin
67
+ When I leave a note
68
+ """
69
+ Please deliver after 5pm.
70
+ """
71
+ Then the saved note should be shown
72
+ ```
73
+
74
+ ## Secrets — `{env:VAR}`
75
+
76
+ ```gherkin
77
+ When I enter "{env:ADMIN_USER}" in the username field
78
+ And I enter "{env:ADMIN_PASSWORD}" in the password field
79
+ ```
80
+
81
+ Resolved from the environment (or a git-ignored `.env`; real env wins) at
82
+ replay. Caches, proposals, reports and history contain only the token;
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
85
+ credentials.
86
+
87
+ ## Dates and dynamic values
88
+
89
+ - Say the intent: "select a check-in date 1 day from today" → recorded
90
+ as `{date+1}`; stays valid every day. Formats: `{date}`, `{date-3}`,
91
+ `{date+1:DD.MM.YYYY}`.
92
+ - Capture and compare displayed values instead of literals:
93
+
94
+ ```gherkin
95
+ When I record the displayed total as "before"
96
+ And I add another night
97
+ Then "before" should differ from the displayed total
98
+ And the total should match "NOK [\d,]+"
99
+ And the "Book" link should point to "/booking"
100
+ ```
101
+
102
+ ## Network-aware steps (no keywords — plain prose)
103
+
104
+ ```gherkin
105
+ When I place the order and wait for the order API to return 201
106
+ And I wait until the job status API reports "READY"
107
+ Then the order request should have returned 201 # assertion, never healed
108
+ ```
109
+
110
+ Recorded as URL-pattern + method + status (+ body pattern) matchers;
111
+ waits are satisfied by responses from the triggering step onward, so
112
+ "click and wait" never races. Prefer these over any fixed sleep.
113
+
114
+ ## Page furniture
115
+
116
+ ```gherkin
117
+ When I click "Clear workspace" and confirm the dialog # alert/confirm/prompt
118
+ When I upload the file "examples/sample.txt" as the attachment
119
+ When I drag the task card onto the done column
120
+ When I enter "Great tool" in the feedback comment and send it # inside an iframe: just interact
121
+ Then the feedback widget should show "Thanks for: Great tool"
122
+ ```
123
+
124
+ Upload paths resolve from the working directory. Links that open new
125
+ tabs are followed automatically.
126
+
127
+ ## StepSets (`.saffron` only)
128
+
129
+ ```gherkin
130
+ Feature: Checkout
131
+
132
+ StepSet: Complete guest information
133
+ Given I am on the guest information page
134
+ When I enter guest name "Chathuranga"
135
+ And I click the continue button
136
+ Then I am not on the guest information page
137
+
138
+ Scenario: Checkout happy path
139
+ Given I am on the cart page
140
+ StepSet Complete guest information
141
+ Then I am on the checkout page
142
+ ```
143
+
144
+ | Form | Syntax | Notes |
145
+ |---|---|---|
146
+ | Define | `StepSet: <name>` | Colon, like `Scenario:`; steps indented below |
147
+ | Invoke | `StepSet <name>` | No colon; sits anywhere among steps |
148
+
149
+ - Expanded at parse time: inlined steps cache and seed like ordinary
150
+ steps; editing a set makes every caller honestly stale (re-recorded
151
+ mostly seeded); heal edits route to the set definition.
152
+ - Names project-wide unique (also catches the colon typo at an
153
+ invocation). Sets may contain tables, doc strings and `<param>`
154
+ placeholders; no nesting; a set never runs standalone.
155
+ - Shared flows go in `features/shared.steps.saffron` — a sets-only
156
+ library file with a `Feature:` header and no scenarios.
157
+ - Assertions inside a set are checkpoints mid-scenario and part of the
158
+ strict final block when the set is invoked last.
159
+
160
+ ## Tags
161
+
162
+ ```gherkin
163
+ @smoke @checkout
164
+ Scenario: ...
165
+ ```
166
+
167
+ `saffron run --filter @smoke`.