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/README.md +23 -23
- package/dist-pkg/cli.js +93 -90
- package/package.json +2 -1
- package/skills/saffron/SKILL.md +18 -17
- package/skills/saffron/references/config.md +6 -5
- package/skills/saffron/references/syntax.md +5 -5
- package/textmate/saffron/language-configuration.json +16 -0
- package/textmate/saffron/package.json +31 -0
- package/textmate/saffron/syntaxes/saffron.tmLanguage.json +127 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saffron-ai",
|
|
3
|
-
"version": "0.4.
|
|
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
|
]
|
package/skills/saffron/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: saffron
|
|
3
|
-
description: Write, run, and maintain end-to-end UI tests with 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
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
55
|
-
- Dates: write intent, not literals
|
|
56
|
-
- Dynamic display values: capture and compare
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|