quicke2e 0.2.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/LICENSE +21 -0
- package/README.md +560 -0
- package/bin/quicke2e.mjs +118 -0
- package/codegen/codegen.mjs +240 -0
- package/package.json +58 -0
- package/skill/quicke2e/SKILL.md +87 -0
- package/src/discover-pattern.mjs +8 -0
- package/src/discover.mjs +254 -0
- package/src/loop.mjs +748 -0
- package/src/redact.mjs +89 -0
- package/src/route.mjs +98 -0
- package/src/secret.mjs +94 -0
- package/src/seen-text.js +72 -0
- package/src/snapshot.js +246 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Moka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
|
|
4
|
+
<img alt="QuickE2E" src="docs/logo.svg" width="420">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<p align="center">
|
|
9
|
+
<b>Plain-English end-to-end tests for web apps.<br>A small decision model picks each click. Code decides pass or fail.</b>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="#quick-start">Quick start</a> ·
|
|
14
|
+
<a href="#how-it-works">How it works</a> ·
|
|
15
|
+
<a href="#benchmarks">Benchmarks</a> ·
|
|
16
|
+
<a href="#engines">Engines</a> ·
|
|
17
|
+
<a href="#limits">Limits</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://www.npmjs.com/package/quicke2e"><img alt="npm" src="https://img.shields.io/npm/v/quicke2e"></a>
|
|
22
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
|
|
23
|
+
<a href="https://github.com/dmoka/quicke2e/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/dmoka/quicke2e/actions/workflows/ci.yml/badge.svg?branch=main"></a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center"><sub>For AI agents: <a href="AGENTS.md"><code>AGENTS.md</code></a> (spec rules, exit codes, the <code>--json</code> record) · <a href="llms.txt"><code>llms.txt</code></a> · <a href="skill/quicke2e/SKILL.md">agent skill</a></sub></p>
|
|
27
|
+
|
|
28
|
+
<br>
|
|
29
|
+
<br>
|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
33
|
+
*Recorded at 1× speed. Task: from the TicketBay home page, buy 2 tickets with the discount code WELCOME10.*
|
|
34
|
+
|
|
35
|
+
| launch task, 5 runs per arm | median wall time | pass | cost / run |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| **QuickE2E, `local` engine, Shisa DE-1** | **3.23 s** | 5/5 | **$0** |
|
|
38
|
+
| **QuickE2E, hosted Jev** | **4.79 s** | 5/5 | $0.00030 |
|
|
39
|
+
| Claude Code + Playwright MCP, Sonnet 5 | 20.89 s (18.4–37.3) | 5/5 | $0.0888 |
|
|
40
|
+
|
|
41
|
+
Hosted Jev against Claude Code: **4.3× faster and 296× cheaper.** Same start page, same goal, one SQL
|
|
42
|
+
check for every run. Measured 2026-09-29 (Jev, Claude Code) and 2026-09-30 (Shisa DE-1) on an M2 Max.
|
|
43
|
+
Raw runs: [`bench/results/launch-task.json`](bench/results/launch-task.json). [Method and other tasks](#benchmarks).
|
|
44
|
+
|
|
45
|
+
## What it is
|
|
46
|
+
|
|
47
|
+
QuickE2E is an exploratory end-to-end browser tester. A spec holds a goal in plain English, the values
|
|
48
|
+
to type, and an assertion. Each step, QuickE2E turns the page into a list of legal moves, a decision
|
|
49
|
+
model returns the key of one move, and code checks the assertion on every page snapshot.
|
|
50
|
+
|
|
51
|
+
- **No selectors in the spec.** A spec holds a goal, inputs and assertions.
|
|
52
|
+
- **The engine writes no text.** It returns one of the offered keys. Every typed value comes from the spec's `inputs`.
|
|
53
|
+
- **Code decides pass or fail.** It checks the URL, the text a user sees, and control state.
|
|
54
|
+
- **`--emit` turns a passing run into a Playwright spec.** The spec replays in CI with no model call.
|
|
55
|
+
- **Secret-looking spec values are scrubbed** from every engine request, and from traces, maps and emitted specs on disk.
|
|
56
|
+
- **The `local` engine costs $0 per run** and makes no network call after the first model download. It needs Apple Silicon.
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
// quicke2e.spec.mjs (the book-with-code spec from examples/ticketbay/flows.mjs)
|
|
60
|
+
export default [{
|
|
61
|
+
name: "book-with-code",
|
|
62
|
+
start: "/events/midnight-arcade-neon-tour",
|
|
63
|
+
maxSteps: 16,
|
|
64
|
+
inputs: { name: "Alex Fan", email: "fan@example.com", "discount code": "WELCOME10" },
|
|
65
|
+
goal: "Book tickets for this event: continue to checkout, apply the discount code WELCOME10, "
|
|
66
|
+
+ "enter the email fan@example.com and the name Alex Fan, and pay.",
|
|
67
|
+
expectUrl: "/orders/\\d+\\?placed=1", // the assertion
|
|
68
|
+
expect: ["Payment confirmed", "WELCOME10 10%", "Total paid €109.39"], // the assertion
|
|
69
|
+
}];
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start
|
|
73
|
+
|
|
74
|
+
Requires Node 20 or later.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install -D quicke2e && npx playwright install chromium
|
|
78
|
+
export OPENROUTER_API_KEY=... # the default jev engine calls OpenRouter
|
|
79
|
+
npx quicke2e run quicke2e.spec.mjs --base http://localhost:3000
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
On TicketBay, the spec above printed this (hosted Jev, 2026-09-24):
|
|
83
|
+
|
|
84
|
+
```console
|
|
85
|
+
$ npx quicke2e run quicke2e.spec.mjs --base http://localhost:3200
|
|
86
|
+
PASS book-with-code 8 steps 4.3s $0.00032 DONE_VERIFIED
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
No app at hand? Clone the repo and run three example flows (`login`, `choose-a-plan`,
|
|
90
|
+
`weekly-digest-toast`) against the bundled fixture pages:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
git clone https://github.com/dmoka/quicke2e && cd quicke2e && npm ci && npx playwright install chromium
|
|
94
|
+
node fixtures/serve.mjs 8899 &
|
|
95
|
+
npx quicke2e run examples/fixtures.spec.mjs --base http://127.0.0.1:8899
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### On your own app
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx quicke2e discover http://localhost:3000 -o quicke2e.map.json # map the app (once, no model)
|
|
102
|
+
npx quicke2e check quicke2e.spec.mjs --base http://localhost:3000 # reject weak assertions
|
|
103
|
+
npx quicke2e run quicke2e.spec.mjs --base http://localhost:3000 --map quicke2e.map.json
|
|
104
|
+
npx quicke2e run quicke2e.spec.mjs --base http://localhost:3000 --emit e2e/generated/ # -> Playwright spec
|
|
105
|
+
npx quicke2e run quicke2e.spec.mjs --base http://localhost:3000 --video runs/ # record the browser
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The browser is visible when you run a command in a terminal. It runs headless when `CI` is set, when
|
|
109
|
+
the output is piped, or on Linux with no `DISPLAY` or `WAYLAND_DISPLAY`. `--headed` and `--headless`
|
|
110
|
+
force either mode.
|
|
111
|
+
|
|
112
|
+
`run` and `check` exit with code 1 when any spec fails or fails the `WEAK_ASSERTION` check.
|
|
113
|
+
|
|
114
|
+
## How it works
|
|
115
|
+
|
|
116
|
+
QuickE2E has three parts. Only the second part calls a model.
|
|
117
|
+
|
|
118
|
+
### 1. Discover (once)
|
|
119
|
+
|
|
120
|
+
`quicke2e discover` crawls the app with plain Playwright and writes a map: pages (`/events/:id`), the
|
|
121
|
+
links and buttons between them, and every form with its fields, options, and the page its submit leads
|
|
122
|
+
to.
|
|
123
|
+
|
|
124
|
+
On `localhost`, `127.0.0.1`, `[::1]` and `0.0.0.0`, discover submits forms (a full crawl), so point it
|
|
125
|
+
at a throwaway database. `--reset "<cmd>"` restores your seed data first. On any other host, a full
|
|
126
|
+
crawl stops with an error unless you pass `--i-own-this-data`. Pass `--safe` for a crawl that submits
|
|
127
|
+
no form. Neither mode clicks a log-out control.
|
|
128
|
+
|
|
129
|
+
### 2. Decide
|
|
130
|
+
|
|
131
|
+
Each step, the page becomes a short list of legal moves (`TYPE_TEXT 3 Email [textbox]`,
|
|
132
|
+
`CLICK 7 Pay [button]`). The engine returns one of the offered keys, with a confidence. It cannot
|
|
133
|
+
invent an action, a selector or a value. QuickE2E treats an answer that is not an offered key as
|
|
134
|
+
BLOCKED and does not act on it.
|
|
135
|
+
|
|
136
|
+
When a page has more candidates than one decision can hold, QuickE2E splits them into heats. The
|
|
137
|
+
engine decides the heats in parallel, each with a "none of these" option, and then decides a final
|
|
138
|
+
between the heat winners. No candidate is dropped.
|
|
139
|
+
|
|
140
|
+
With a map (`--map`), the engine first picks the page the goal needs. If the start page is in the map
|
|
141
|
+
and the map has a link route, code clicks along that route.
|
|
142
|
+
|
|
143
|
+
### 3. Verify
|
|
144
|
+
|
|
145
|
+
Code decides success. It checks the assertions on every snapshot:
|
|
146
|
+
|
|
147
|
+
| assertion | passes when |
|
|
148
|
+
|---|---|
|
|
149
|
+
| `expectUrl` | the URL matches this regex |
|
|
150
|
+
| `expect` | a sighted user sees this text on the page. Hidden, `aria-hidden`, transparent, clipped and off-page text does not count. The values of form controls do not count, because the test typed or chose them |
|
|
151
|
+
| `expectState: [{ role, name, value \| checked \| selected }]` | a control has this state (a chosen option, a checked box, a field's value) |
|
|
152
|
+
| `expectSeen` | this text appeared at any moment since the page loaded, such as a toast |
|
|
153
|
+
|
|
154
|
+
The run stops on the first snapshot where all assertions hold, and that run passes. Write the assertions
|
|
155
|
+
for the state after the last action: after a submit, assert the page the submit leads to.
|
|
156
|
+
|
|
157
|
+
### Text comes from the spec
|
|
158
|
+
|
|
159
|
+
Spec keys match field labels by substring (`email` → "Email"). One key fills one field per page. When
|
|
160
|
+
a label contains no key (a renamed label such as "E-mail" or "Ticket holder"), the engine picks which
|
|
161
|
+
of your unused keys belongs there. That pick is a choice over your keys, so the value still comes only
|
|
162
|
+
from the spec. QuickE2E never maps a key into a number, date or file field this way.
|
|
163
|
+
|
|
164
|
+
### `WEAK_ASSERTION`
|
|
165
|
+
|
|
166
|
+
`run` and `check` first load the start page. If the assertion already holds before any step, QuickE2E
|
|
167
|
+
rejects the spec with `WEAK_ASSERTION`, because an assertion that is true on page load passes without
|
|
168
|
+
any work.
|
|
169
|
+
|
|
170
|
+
### Codegen
|
|
171
|
+
|
|
172
|
+
`--emit <dir>` turns a passing run into `<dir>/<name>.spec.ts`: a Playwright spec with accessible-name
|
|
173
|
+
locators (`getByRole`) and the same assertions. Each input is read from an environment variable named
|
|
174
|
+
`<FLOW>_<KEY>` (for example `CHECKOUT_PLAIN_EMAIL`). A non-secret input falls back to the spec value. A
|
|
175
|
+
secret input has no fallback, so the credential is never written into the file.
|
|
176
|
+
|
|
177
|
+
## Secrets and redaction
|
|
178
|
+
|
|
179
|
+
**Secret inputs.** A spec key is secret when its name matches a pattern such as `password`,
|
|
180
|
+
`api key`, `card`, `token`, `otp` or `iban`. QuickE2E scrubs a strong secret value (10 or more
|
|
181
|
+
characters with 2 or more character classes, or a number with 10 or more digits) from every engine
|
|
182
|
+
request, including places where the page echoes it: re-cased, truncated, re-spaced, grouped,
|
|
183
|
+
URL-encoded, or as a masked card "ending 6789". It also scrubs the value from traces, maps and emitted
|
|
184
|
+
specs on disk.
|
|
185
|
+
|
|
186
|
+
**Weak secret values.** A weak value (`admin`, `letmein1`) looks like an ordinary word, so QuickE2E
|
|
187
|
+
classifies page strings by history:
|
|
188
|
+
- A page string that appears only after the value was typed is an echo. QuickE2E scrubs it.
|
|
189
|
+
- A string the page had before, or a label that is exactly that word (an "Admin" link), is the page's own text. QuickE2E keeps it, and it reaches the engine.
|
|
190
|
+
|
|
191
|
+
`run` prints a note for every weak secret in a spec.
|
|
192
|
+
|
|
193
|
+
**Page content.** The engine decides on page content: labels, the page title, and form values. Declare
|
|
194
|
+
content that must never leave the page in the spec:
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
redact: [".backup-code", "#saved-cards", /recovery code \S+/i] // CSS selectors and text patterns
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- A selector covers its elements in the document, open shadow roots and same-origin iframes, and every label, name or option derived from their text.
|
|
201
|
+
- A pattern covers raw text, such as the page title and URLs.
|
|
202
|
+
- `discover --redact` takes the same list, so the map on disk stays clean.
|
|
203
|
+
- A matched text with fewer than 4 letters or digits (a 3-digit CVC) hides its own element, but QuickE2E does not search for it in other text, because cutting every "737" on a page would corrupt prices and numbers. Declare such a copy with a pattern.
|
|
204
|
+
|
|
205
|
+
## Benchmarks
|
|
206
|
+
|
|
207
|
+
Machine: an M2 Max. Hosted engine: `jev` (`typesafe/jev-1.13` via OpenRouter). Claude's cost is the
|
|
208
|
+
`total_cost_usd` that `claude -p --output-format json` reports (API-equivalent). Wall time is the whole
|
|
209
|
+
command, end to end, including browser or MCP start-up.
|
|
210
|
+
|
|
211
|
+
### Launch task: buy from the home page (the GIF)
|
|
212
|
+
|
|
213
|
+
The agent starts on the home page, finds the event in the list, opens it, continues to checkout,
|
|
214
|
+
applies WELCOME10, enters the email and name, and pays. The check for every run: one new paid order,
|
|
215
|
+
WELCOME10 applied, total €109.39. TicketBay in its dark theme, measured 2026-09-30.
|
|
216
|
+
|
|
217
|
+
| | pass | median wall | steps | cost / run |
|
|
218
|
+
|---|---|---|---|---|
|
|
219
|
+
| **QuickE2E, `local` engine, Shisa DE-1** | **5/5** | **3.23 s** | 7 | **$0** |
|
|
220
|
+
| **QuickE2E, `jev` engine** | **5/5** | **4.79 s** | 7 | $0.00030 |
|
|
221
|
+
| Claude Code + Playwright MCP, Sonnet 5 | 5/5 | 20.89 s (18.4–37.3) | 13–15 tool calls | $0.0888 |
|
|
222
|
+
|
|
223
|
+
Hosted Jev against Sonnet 5: 20.89 / 4.79 = 4.36 (4.3× faster); $0.0888 / $0.00030 = 296 (296×
|
|
224
|
+
cheaper). Median decision time: 94 ms on Shisa DE-1 (local, no network), 327 ms on hosted Jev.
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary>App version and reproduce</summary>
|
|
228
|
+
|
|
229
|
+
TicketBay here is the [`bench/2026-10`](https://github.com/dmoka/ticket-bay/tree/bench/2026-10) branch:
|
|
230
|
+
the benchmark commit `7bc02b6` plus one checkout fix. With the fix, the discount-code form no longer
|
|
231
|
+
reloads the page and wipes the typed details. The same fix is `e40d86c` on TicketBay `main`. Before
|
|
232
|
+
that fix, QuickE2E typed the email and name a second time after applying the code.
|
|
233
|
+
|
|
234
|
+
Run TicketBay `bench/2026-10` on :3200 as a production build, and set `APP_DIR` and `DATABASE_URL` for
|
|
235
|
+
`examples/ticketbay/reset.sh`. Then:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
node bench/demo-capture.mjs --arm jev --n 5 --dark --spec bench/demo-flows.mjs --flow buy-from-home --out runs/demo
|
|
239
|
+
node bench/demo-capture.mjs --arm jev --engine local --n 5 --dark --spec bench/demo-flows.mjs --flow buy-from-home --out runs/demo
|
|
240
|
+
node bench/demo-capture.mjs --arm claude --model sonnet --n 5 --dark --spec bench/demo-flows.mjs --flow buy-from-home --out runs/demo
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The `--engine local` line needs the local engine server running with `--model shisa-de-1`. Each run
|
|
244
|
+
writes a video and a timeline (steps, timestamps, tokens, cost), and the script prints the median wall
|
|
245
|
+
time and median cost. The script launches and records Claude Code's browser, and the MCP server reaches
|
|
246
|
+
that browser over CDP, so the Claude Code video is complete.
|
|
247
|
+
|
|
248
|
+
</details>
|
|
249
|
+
|
|
250
|
+
### Plain checkout: start on the event page (2026-09-24)
|
|
251
|
+
|
|
252
|
+
A shorter task: start on the event page, continue to checkout, enter the email and name, and pay (no
|
|
253
|
+
discount code). Same goal text for every arm, a database reset before every run, and one SQL check: a
|
|
254
|
+
new paid order with the right name, email and total.
|
|
255
|
+
|
|
256
|
+
| | pass | median wall | mean cost / run |
|
|
257
|
+
|---|---|---|---|
|
|
258
|
+
| **QuickE2E, `jev` engine** | **5/5** | **3.05 s** | **$0.000153** |
|
|
259
|
+
| QuickE2E, `local` engine, Shisa DE-1 | 5/5 | **2.04 s** | $0 |
|
|
260
|
+
| QuickE2E, `local` engine, Eikos-4B | 5/5 | 4.36 s | $0 |
|
|
261
|
+
| Claude Code + Playwright MCP, Sonnet 5 | 5/5 | 21.67 s | $0.0940 |
|
|
262
|
+
| Claude Code + Playwright MCP, Opus 5.5 | 5/5 | 25.34 s | $0.1033 |
|
|
263
|
+
|
|
264
|
+
Hosted Jev was 7.10× faster and 614× cheaper than Sonnet 5, and 8.30× faster and 675× cheaper than
|
|
265
|
+
Opus 5.5 (median wall time, mean cost). The two `local` rows were measured on 2026-09-28.
|
|
266
|
+
|
|
267
|
+
Hosted latency varies. One 5-run `jev` batch ran during a slow period on the hosted engine and took a
|
|
268
|
+
median 11.6 s (1.86× faster than Sonnet 5). The batch re-run right after it gave the 3.05 s above. The
|
|
269
|
+
`local` engine sends no request over the network, so this variance does not apply to it.
|
|
270
|
+
|
|
271
|
+
<details>
|
|
272
|
+
<summary>Reproduce</summary>
|
|
273
|
+
|
|
274
|
+
Needs a running TicketBay.
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
node bench/headtohead.mjs --arm jev --n 5
|
|
278
|
+
node bench/headtohead.mjs --arm claude --model sonnet --n 5
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Raw runs of the `jev` batches (both) and the Claude Code arms: `bench/results/h2h-final.json`.
|
|
282
|
+
|
|
283
|
+
</details>
|
|
284
|
+
|
|
285
|
+
### Eight UI stacks × three tasks
|
|
286
|
+
|
|
287
|
+
Stacks: vanilla HTML, React + MUI, React + Ant Design, React + Radix/shadcn, Vue 3 + Element Plus, Web
|
|
288
|
+
Components (Shoelace + Lit, shadow DOM), a form inside a same-origin iframe, and a legacy jQuery/table
|
|
289
|
+
page. Tasks: log in; fill a form with a text field, a dropdown and a checkbox; open row 57 of a 60-row
|
|
290
|
+
list.
|
|
291
|
+
|
|
292
|
+
| engine | pass | median run | mean cost / run |
|
|
293
|
+
|---|---|---|---|
|
|
294
|
+
| `jev` | **120/120** (24 cells, n=5 per cell) | 1.8 s | $0.00014 |
|
|
295
|
+
|
|
296
|
+
Raw runs: `bench/results/6c6a550.jsonl`. The page-representation comparison behind this design
|
|
297
|
+
(in-page DOM snapshot vs Playwright's aria snapshot vs the CDP accessibility tree) is in
|
|
298
|
+
[`docs/bakeoff-2026-09-24.md`](docs/bakeoff-2026-09-24.md).
|
|
299
|
+
|
|
300
|
+
<details>
|
|
301
|
+
<summary>Reproduce (about 4 minutes, about $0.017)</summary>
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
bench/stacks/build.sh && node bench/verify-stacks.mjs # builds the 8 apps; 24/24 scripted checks, no model
|
|
305
|
+
node bench/run-matrix.mjs --approaches A --n 5 --out bench/results/mine.jsonl # a NEW --out file
|
|
306
|
+
node bench/report.mjs bench/results/mine.jsonl
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Ports 5101–5108 must be free. `A` means this checkout's `src/`. The runner skips runs already in its
|
|
310
|
+
output file, so always pass a new `--out`.
|
|
311
|
+
|
|
312
|
+
</details>
|
|
313
|
+
|
|
314
|
+
### Four TicketBay specs
|
|
315
|
+
|
|
316
|
+
TicketBay is the practice app of the *AI Agent Engineer* course: Next.js 16 + shadcn/Radix + Postgres,
|
|
317
|
+
production build, data reset before every run. Specs are in `examples/ticketbay/`. The `jev` columns
|
|
318
|
+
are from 2026-09-24, the `local` column from 2026-09-28.
|
|
319
|
+
|
|
320
|
+
| spec | jev | local (Eikos-4B / Shisa DE-1) | steps (jev) | wall (jev) | cost (jev) |
|
|
321
|
+
|---|---|---|---|---|---|
|
|
322
|
+
| book with a discount code | 5/5 | 5/5 / 5/5 | 8 | 4.3 s | $0.00032 |
|
|
323
|
+
| refund inside the window | 5/5 | 5/5 / 5/5 | 1 | 0.9 s | $0.00003 |
|
|
324
|
+
| refund refused after the event started | 5/5 | 5/5 / 5/5 | 1 | 0.9 s | $0.00003 |
|
|
325
|
+
| plain checkout | 5/5 | 5/5 / 5/5 | 4 | 2.5 s | $0.00015 |
|
|
326
|
+
|
|
327
|
+
- **The refund spec found a planted bug.** It fails 5/5 against a copy of TicketBay with the refund-window check removed. That copy refunded €39.69 after the event started.
|
|
328
|
+
- **The emitted spec replays.** The Playwright spec emitted from a passing checkout run replays 3/3, about 0.8 s each including browser start, with no model.
|
|
329
|
+
|
|
330
|
+
### Fixture suite
|
|
331
|
+
|
|
332
|
+
`fixtures/` holds 59 small hand-written pages, run at n=3. Each page is a defect found in the field or
|
|
333
|
+
an attack from the project's ten-round security and robustness audit: shadow DOM, iframes, toasts,
|
|
334
|
+
secret echoes (re-cased, truncated, grouped, URL-encoded, weak), declared redaction through shadow
|
|
335
|
+
roots and iframes, hidden-text false passes, a 150-link page, a safe crawl.
|
|
336
|
+
|
|
337
|
+
| engine | pass |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `jev` | **59/59** |
|
|
340
|
+
| `local`, Eikos-4B | 58/59 |
|
|
341
|
+
| `local`, Shisa DE-1 | 56/59 |
|
|
342
|
+
|
|
343
|
+
Strong spec secrets in the suite: 0 leaks.
|
|
344
|
+
|
|
345
|
+
## Engines
|
|
346
|
+
|
|
347
|
+
Select an engine with `--engine`. Jev is a model by TypeSafe. QuickE2E is an independent project, not affiliated with TypeSafe.
|
|
348
|
+
|
|
349
|
+
| engine | setup | notes |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| `jev` (default) | `OPENROUTER_API_KEY` | Hosted, any OS. |
|
|
352
|
+
| `vercel` | `AI_GATEWAY_API_KEY` | The same Jev model through the Vercel AI Gateway. Not measured for this release. |
|
|
353
|
+
| `local` | `local-engine/server.py` | **Apple Silicon only.** $0 per run. Open decision models that read the logits of the option labels and generate no text. |
|
|
354
|
+
|
|
355
|
+
The `local` engine runs one of two open models:
|
|
356
|
+
|
|
357
|
+
| | **Eikos-4B** (default) | **Shisa DE-1** (`--model shisa-de-1`) | Jev (hosted) |
|
|
358
|
+
|---|---|---|---|
|
|
359
|
+
| what it is | Qwen3.5-4B fine-tuned for decisions | Gemma 4 26B MoE, 3.8B active per token | TypeSafe's hosted model |
|
|
360
|
+
| peak memory | **4.1 GB** | 17.4 GB | none locally |
|
|
361
|
+
| Mac | any Apple Silicon Mac with 8 GB or more | 32 GB, and `brew install llama.cpp` | any OS |
|
|
362
|
+
| TicketBay (4 specs × 5) | 20/20 | 20/20 | 20/20 |
|
|
363
|
+
| plain checkout, end to end | 4.4 s | **2.0 s** | 3.05 s |
|
|
364
|
+
| fixture suite (59 pages, n=3) | 58/59 | 56/59 | 59/59 |
|
|
365
|
+
|
|
366
|
+
Measured 2026-09-28 on an M2 Max with 64 GB. Shisa DE-1 beats hosted Jev on the plain checkout because
|
|
367
|
+
only 3.8B parameters are active per token and no request crosses the network.
|
|
368
|
+
|
|
369
|
+
<details>
|
|
370
|
+
<summary>Run the local engine</summary>
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
cd local-engine
|
|
374
|
+
uv venv --python 3.12 .venv
|
|
375
|
+
VIRTUAL_ENV=.venv uv pip install -r requirements.txt
|
|
376
|
+
.venv/bin/python server.py # Eikos-4B (default), :8822
|
|
377
|
+
.venv/bin/python server.py --model shisa-de-1 # Shisa DE-1 (needs: brew install llama.cpp)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Then run `quicke2e run ... --engine local`. Set `LOCAL_URL` if the server uses another port. The first
|
|
381
|
+
start downloads the model. `GET /` reports the model, peak memory and decisions served. Details,
|
|
382
|
+
prompt format and model licences: [`local-engine/README.md`](local-engine/README.md).
|
|
383
|
+
|
|
384
|
+
</details>
|
|
385
|
+
|
|
386
|
+
## The agent skill: a big model invents the cases
|
|
387
|
+
|
|
388
|
+
`skill/quicke2e/SKILL.md` is an agent skill for Claude Code and compatible agents. The big model:
|
|
389
|
+
|
|
390
|
+
1. reads the map and your source code,
|
|
391
|
+
2. lists the business rules (`rule — file:line`),
|
|
392
|
+
3. invents happy-path, boundary and refusal cases,
|
|
393
|
+
4. writes the spec file and runs `check` and `run`,
|
|
394
|
+
5. reports which failures are app bugs.
|
|
395
|
+
|
|
396
|
+
The big model runs once to write the specs. The decision engine makes every decision of every run.
|
|
397
|
+
Code decides pass or fail.
|
|
398
|
+
|
|
399
|
+
## Reference
|
|
400
|
+
|
|
401
|
+
<details>
|
|
402
|
+
<summary>CLI</summary>
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
quicke2e discover <baseUrl> [--start /,/admin] [--safe] [--i-own-this-data] [--reset "<cmd>"]
|
|
406
|
+
[--inputs inputs.json] [--storage state.json] [-o quicke2e.map.json]
|
|
407
|
+
[--redact ".css-selector" --redact "/regex/i" ...] [--headed|--headless]
|
|
408
|
+
quicke2e run <spec.mjs> [--base url] [--engine jev|local|vercel] [--map quicke2e.map.json]
|
|
409
|
+
[--runs N] [--emit dir] [--trace dir] [--video dir] [--headed|--headless]
|
|
410
|
+
[--allow-weak] [--only name]
|
|
411
|
+
quicke2e check <spec.mjs> [--base url]
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
| flag | command | effect |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| `--start` | discover | comma-separated start paths (default `/`) |
|
|
417
|
+
| `--safe` | discover | safe crawl: follows links, opens menus, dialogs and dropdowns, and never submits a form (see [Limits](#limits)) |
|
|
418
|
+
| `--i-own-this-data` | discover | allow a full crawl on a host other than `localhost` |
|
|
419
|
+
| `--reset "<cmd>"` | discover | command that restores seed data before the crawl |
|
|
420
|
+
| `--inputs` | discover | JSON file with values for forms during the crawl |
|
|
421
|
+
| `--storage` | discover | Playwright storage-state file |
|
|
422
|
+
| `--max-pages` | discover | page limit for the crawl (default 40) |
|
|
423
|
+
| `-o` | discover | output map file (default `quicke2e.map.json`) |
|
|
424
|
+
| `--redact` | discover | CSS selector or `/regex/`; repeat the flag for more |
|
|
425
|
+
| `--base` | run, check | app base URL (default `$APP_BASE`, then `http://localhost:3000`) |
|
|
426
|
+
| `--engine` | run | `jev` (default), `local` or `vercel` |
|
|
427
|
+
| `--map` | run | map file from `discover` |
|
|
428
|
+
| `--runs` | run | runs per spec (default 1) |
|
|
429
|
+
| `--only` | run, check | run only the spec with this `name` |
|
|
430
|
+
| `--emit` | run | write a Playwright spec for each passing run |
|
|
431
|
+
| `--trace` | run | write a JSON trace per run, with each step's start time and decision time |
|
|
432
|
+
| `--video` | run | save a WebM per run. With `--trace`, each step in the trace also gets the box of the element it acted on. The video shows typed values |
|
|
433
|
+
| `--allow-weak` | run | run a spec that failed the `WEAK_ASSERTION` check |
|
|
434
|
+
| `--json` | run | print all run records as one JSON array on the last line of stdout (fields: [`AGENTS.md`](AGENTS.md#run-and-read-the-result)) |
|
|
435
|
+
| `--headed` / `--headless` | all | force the browser mode |
|
|
436
|
+
|
|
437
|
+
</details>
|
|
438
|
+
|
|
439
|
+
<details>
|
|
440
|
+
<summary>Spec fields</summary>
|
|
441
|
+
|
|
442
|
+
A spec file exports an array of flows (`export default [...]`).
|
|
443
|
+
|
|
444
|
+
| field | meaning |
|
|
445
|
+
|---|---|
|
|
446
|
+
| `name` | flow name, used in output, `--only` and emitted file names |
|
|
447
|
+
| `start` | start path (default `/`) |
|
|
448
|
+
| `base` | base URL for this flow; overrides `--base` |
|
|
449
|
+
| `goal` | the task in plain English |
|
|
450
|
+
| `inputs` | every value the run types, keyed by field label |
|
|
451
|
+
| `expectUrl`, `expect`, `expectState`, `expectSeen` | the assertions (see [Verify](#3-verify)) |
|
|
452
|
+
| `redact` | CSS selectors and text patterns the engine must never see |
|
|
453
|
+
| `storageState` | Playwright storage state for the browser context |
|
|
454
|
+
| `maxSteps` | step limit (default 14) |
|
|
455
|
+
| `done` | optional plain-English end state, for the reader. The run loop does not send it to the engine |
|
|
456
|
+
|
|
457
|
+
</details>
|
|
458
|
+
|
|
459
|
+
<details>
|
|
460
|
+
<summary>Run outcomes</summary>
|
|
461
|
+
|
|
462
|
+
The outcome says why the run loop stopped. Pass or fail comes from the final assertion check.
|
|
463
|
+
|
|
464
|
+
| outcome | meaning |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `DONE_VERIFIED` | the assertions held on a snapshot |
|
|
467
|
+
| `MODEL_BLOCKED` | the engine answered BLOCKED (or a key that was not offered) three times, each time on a page that did not change within 3 s |
|
|
468
|
+
| `NO_SPEC_VALUE` | the run needed a value the spec does not have |
|
|
469
|
+
| `MAX_STEPS` | the step limit ran out |
|
|
470
|
+
| `ERROR` | the run threw an error |
|
|
471
|
+
|
|
472
|
+
</details>
|
|
473
|
+
|
|
474
|
+
## Limits
|
|
475
|
+
|
|
476
|
+
- **QuickE2E checks only the assertions in the spec.** A green run proves those assertions and nothing else about the app.
|
|
477
|
+
- **Use the emitted Playwright spec as the CI merge gate.** A model-driven run can take a different path on the next run.
|
|
478
|
+
- **Tested stacks:** the eight above plus TicketBay. Not tested: canvas apps, cross-origin iframes (Stripe Elements), closed shadow roots, native mobile.
|
|
479
|
+
- **The step count can vary.** Page timing can add a step. The verdict comes from code, so the same final page gives the same verdict.
|
|
480
|
+
- **Page content reaches the engine unless you declare it with `redact`.** No rule guesses which page text is sensitive. An automatic "looks like a code" rule was built and measured: it broke four working flows (order numbers, versions, SKUs, a year) and still missed other code shapes.
|
|
481
|
+
- **A full crawl changes data.** Use a throwaway database.
|
|
482
|
+
- **`--safe` is best effort.** It clicks only controls that declare they open something (menus, tabs, dropdowns) and skips links whose text or URL names a destructive verb. A GET link or an opener with a side effect can still change data.
|
|
483
|
+
- **The `local` engines fail more fixtures than Jev** (58/59 and 56/59 against 59/59). Eikos-4B is slower than Jev (a plain checkout in 4.4 s against 3.05 s). Shisa DE-1 is faster but needs a 32 GB Mac.
|
|
484
|
+
|
|
485
|
+
<details>
|
|
486
|
+
<summary>What does a run cost?</summary>
|
|
487
|
+
|
|
488
|
+
- Hosted `jev`: the sum of the `usage.cost` that OpenRouter reports per decision. Measured: $0.00003 (a 1-step refund spec) to $0.00032 (an 8-step checkout) on TicketBay, and a mean of $0.00014 on the eight-stack matrix.
|
|
489
|
+
- `local`: $0.
|
|
490
|
+
- `discover` and `check` call no model, so they cost $0.
|
|
491
|
+
- The agent skill runs on your own agent, at that agent's price.
|
|
492
|
+
|
|
493
|
+
</details>
|
|
494
|
+
|
|
495
|
+
<details>
|
|
496
|
+
<summary>What data leaves my machine?</summary>
|
|
497
|
+
|
|
498
|
+
With `jev` or `vercel`, each decision sends one request to OpenRouter or the Vercel AI Gateway. It
|
|
499
|
+
holds:
|
|
500
|
+
- the URL with token-like path segments replaced by `:id`, and the first 60 characters of the page title,
|
|
501
|
+
- the offered elements: role and label (up to 44 characters each),
|
|
502
|
+
- form values: a chosen option, a checkbox state, or a typed value that is exactly a non-secret spec value. Any other filled field is sent as `(filled)`,
|
|
503
|
+
- the last 5 steps, the goal, and the list of legal moves,
|
|
504
|
+
- for a field whose label matches no spec key: the label and the names of your unused spec keys (the values stay local).
|
|
505
|
+
|
|
506
|
+
Strong secret values are scrubbed from all of it. Content you declare in `redact` is removed before the
|
|
507
|
+
request is built. With `--map`, one or two extra requests send the app's host name and a summary of each
|
|
508
|
+
mapped page (path pattern, heading, form fields). With `local`, requests go only to the local server
|
|
509
|
+
(`127.0.0.1:8822` by default). `discover` and the emitted Playwright spec call no model.
|
|
510
|
+
|
|
511
|
+
</details>
|
|
512
|
+
|
|
513
|
+
<details>
|
|
514
|
+
<summary>Which API keys do I need?</summary>
|
|
515
|
+
|
|
516
|
+
| engine | key |
|
|
517
|
+
|---|---|
|
|
518
|
+
| `jev` | `OPENROUTER_API_KEY` |
|
|
519
|
+
| `vercel` | `AI_GATEWAY_API_KEY` |
|
|
520
|
+
| `local` | none |
|
|
521
|
+
|
|
522
|
+
`discover`, `check` and `npm test` need no key.
|
|
523
|
+
|
|
524
|
+
</details>
|
|
525
|
+
|
|
526
|
+
<details>
|
|
527
|
+
<summary>How do I use it in CI?</summary>
|
|
528
|
+
|
|
529
|
+
1. Run `quicke2e run ... --emit e2e/generated/` locally until the spec passes.
|
|
530
|
+
2. Commit the emitted `<name>.spec.ts` and run it with `npx playwright test`. It calls no model.
|
|
531
|
+
3. Set `APP_BASE` to the app's URL, and set one environment variable per secret input (`<FLOW>_<KEY>`, for example `LOGIN_PASSWORD`).
|
|
532
|
+
|
|
533
|
+
`quicke2e run` also works in CI. It runs headless when `CI` is set and exits with code 1 when a spec
|
|
534
|
+
fails.
|
|
535
|
+
|
|
536
|
+
</details>
|
|
537
|
+
|
|
538
|
+
## Design notes
|
|
539
|
+
|
|
540
|
+
<details>
|
|
541
|
+
<summary>What we measured while building it</summary>
|
|
542
|
+
|
|
543
|
+
- **Bad options are removed from the list.** A prompt instruction does not stop a decision model from picking a bad option, so the tool removes the option: no DONE option at all, no submit while a spec field is unset, no typing into a field the spec has no value for.
|
|
544
|
+
- **Form state goes in a `values` map.** The same fact as a non-choosable element in the choice list made Jev answer BLOCKED (0.37). As a `values` map, Jev clicked Save (0.99).
|
|
545
|
+
- **Code judges success.** The deterministic check runs on every snapshot, and the model is never asked whether the task is done. This removed every false DONE in the measured runs.
|
|
546
|
+
|
|
547
|
+
</details>
|
|
548
|
+
|
|
549
|
+
## Development
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
npm test # the model-free test suite: no key, no cost
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
On every push and pull request, CI runs the model-free suite and the local-engine prompt tests. When
|
|
556
|
+
the repo has an `OPENROUTER_API_KEY` secret, CI also runs the fixture flows on the hosted Jev engine.
|
|
557
|
+
|
|
558
|
+
## License
|
|
559
|
+
|
|
560
|
+
MIT
|