End-to-end test automation · MIT · local-first
Tests you read.
Docs you run.
ronsel turns every cross-system test into a Markdown notebook. Read it, review it in a PR, and run it against your real websites, APIs, databases and message brokers — locally or in CI.
$ npm install -g ronselFor your manager: every test case is a document anyone can audit, and the pipeline executes it.
An example flow
One document. Three systems. One answer.
This is a real flow. A delivery fails and the parcel is rescheduled; it checks that the tracking API, the warehouse database and the picking screens still agree. Press Run — the executions are pre-recorded, nothing leaves this page.
A failed delivery is rescheduled
When the courier reports the recipient absent, the parcel is queued for a second attempt and the warehouse is told.
A parcel leaves the warehouse and the courier cannot hand it over: nobody is home. Three systems have to agree on what happens next. The tracking API must accept the failed attempt, the warehouse database must schedule a second one, and the pickers must hear about it over MQTT. This is the flow that proves all three still agree. It runs against the real warehouse, with the courier faked.
The parcel ships
A new shipment for a random order. The courier's label service is mimicked, so this step is the same on a laptop with no VPN as it is in staging.
application: wms
method: createShipment
parameters:
body:
orderRef: "ORD-{{ randomInt0_9999 }}"
recipient: "{{ randomName }}"
postalCode: "{{ randomPostalCode }}"
service: next-day
mimic:
- application: carrier
url: "/labels"
test:
status: 201
body:
status: "in_transit"
trackingRef: "$expr: typeof value === 'string' && value.length > 0"● Passed · 212 ms · HTTP 201
{ "shipmentId": "SHP-70491", "orderRef": "ORD-3817",
"status": "in_transit", "trackingRef": "TRK-9C41V2" }✓ status 201✓ body.status✓ body.trackingRef ($expr)
memory.shipmentId ← "SHP-70491"
The warehouse agrees
The API said in_transit. The database is where the pickers actually look, so ask it directly. {{ shipmentId }} is what the previous step wrote to flow memory.
application: warehouse-db
method: query
parameters:
body:
sql: "select status, attempts from shipments where id = $1"
values:
- "{{ shipmentId }}"
test:
status: 200
body:
rows:
0:
status: "in_transit"
attempts: 1● Passed · 8 ms · 1 row
{ "rows": [ { "status": "in_transit", "attempts": 1 } ] }✓ status 200✓ body.rows.0.status✓ body.rows.0.attempts
The courier finds nobody home
The failure we came for. The response is immediate, but the interesting part is asynchronous: an event on wms/shipments/{{ shipmentId }} that the picking screens subscribe to. The MQTT client has been subscribed since before the flow started, so nothing is missed while the HTTP call is in flight.
application: wms
method: reportDeliveryAttempt
parameters:
params:
shipmentId: "{{ shipmentId }}"
body:
outcome: RECIPIENT_ABSENT
courierId: "{{ randomPostmanId }}"
test:
status: 202
latentApplications:
- application: mqtt
client: warehouse
test:
- topic: "wms/shipments/{{ shipmentId }}"
message:
status: "reattempt_scheduled"
retry:
attempts: 3
delay: 2● Running · HTTP 202 accepted · waiting on wms/shipments/SHP-70491
latent · mqtt · attempt 1/3 — no message yet, retrying in 2 s
● Running · HTTP 202 accepted · waiting on wms/shipments/SHP-70491
latent · mqtt · attempt 1/3 — no message yet, retrying in 2 s
latent · mqtt · attempt 2/3 — message received on wms/shipments/SHP-70491
● Passed · 2.3 s · HTTP 202 + MQTT event
{ "accepted": true }
mqtt wms/shipments/SHP-70491 → { "status": "reattempt_scheduled" }✓ status 202✓ mqtt topic wms/shipments/SHP-70491✓ message.status
And tomorrow it goes out again
Second attempt booked, and booked for a future date — not for the morning that has already gone.
application: warehouse-db
method: query
parameters:
body:
sql: "select status, attempts, next_attempt_at from shipments where id = $1"
values:
- "{{ shipmentId }}"
test:
status: 200
body:
rows:
0:
status: "reattempt_scheduled"
attempts: 2
next_attempt_at: "$expr: new Date(value) > new Date()"● Passed · 6 ms · 1 row
{ "rows": [ { "status": "reattempt_scheduled", "attempts": 2,
"next_attempt_at": "2026-09-07T07:30:00Z" } ] }✓ status 200✓ body.rows.0.status✓ body.rows.0.attempts✓ next_attempt_at ($expr)
Nobody had to open three tools to know whether this behaviour still holds. It is one document, and it just ran.
The problem
Test knowledge shouldn't live in one engineer's code
Locked in code
Your best engineer wrote the suite. Only they can read it. When they move on, your coverage becomes a black box.
For your manager: our tests stop depending on one person.
Parked in Jira
Your QA team wrote the test cases. They are precise, reviewed and current. None of them has ever executed.
For your manager: the cases we already wrote start running in CI.
Split across systems
The real flow crosses a web UI, an API, a database and a message broker. Each tool tests one piece alone. The bug lives in between.
For your manager: one test follows the whole flow, end to end.
One artifact does both jobs. A document people read. A test the pipeline runs.
How it works
Results land where the question was asked
A flow is a Markdown file: frontmatter, prose, and step blocks. You read a step, you run it, and the evidence appears below it. The same document runs headless in your pipeline. One command, one exit code.
Frontmatter for owner, priority, tags. Prose for people. YAML step blocks for the runner.
HTTP APIs, PostgreSQL, MQTT and, through Playwright, the browser. Dependencies you can't touch are mimicked per step.
Request, response, timings and every assertion, inline. No console, no report to cross-reference.
The environment is a flag, not a rewrite. One command, one exit code.
wms · createShipment✓ passed212 ms
warehouse-db · query✓ passed8 ms
wms · reportDeliveryAttempt✓ passed2.3 s · mqtt attempt 2/3
warehouse-db · query✓ passed6 ms
4 steps, 0 failures
$ echo $?→ 0
running
For your manager: the test report is the test itself.
The tool
What it looks like on your screen
The document, the folder it lives in, and the two ways AI writes one for you — all local, all yours.
Capabilities
What a step can do
application: wmsHTTP APIsCall any method of your applications. Assert on status, headers and body.
method: queryPostgreSQLAsk the database directly when the API is not the only source of truth.
latentApplications:MQTTSubscribe before the flow starts and assert on what arrives, whenever it arrives.
application: browserBrowserDrive a website through Playwright in the same flow as your API and database steps. Experimental.
mimic:MimicFake a dependency per step, failure scenarios included.
{{ randomName }}Random dataFresh names, ids and dates on every attempt, from built-in replacers.
$expr: value > 0Memory & $exprSteps share what they learn. Assertions go beyond equality.
retry: attempts: 3Retry & delayAsync systems get a second look, not a flake.
--env stagingEnvironmentsCredentials live in env files per application, out of the repository.
xray.testKey: WMS-482Jira · XrayPull your Tests from Xray; each becomes a flow that runs in CI. Read-only, never writes to Jira.
ollama · gemini · claudeWritten with AIDescribe the scenario and get a flow built from your own applications. Local Ollama, Gemini or Claude.
Coverage
Coverage your whole team can see
Every property in a flow's frontmatter — owner, priority, due, anything you invent — becomes a column. Folders render as tables you can sort, filter and save as views, in one views.yaml your team keeps in git.
| name | owner | priority ↓ | tags | reviewed | due | coverage* |
|---|---|---|---|---|---|---|
| proof-of-delivery.md | jose | 9 | pod, critical | — | 2026-08-25 | shallow |
| failed-delivery-retry.md | ana | 8 | last-mile, smoke | ✓ | 2026-09-30 | deep |
| cod-cash-collection.md | marta | 7 | cod, billing | ✓ | 2026-09-08 | deep |
| address-correction.md | lucia | 5 | edge-case | — | 2026-09-15 | shallow |
* a formula: if(flow.steps > 3, "deep", "shallow") — computed columns, filters and saved views live in views.yaml
For your manager: release readiness is a saved view, not a meeting.
Security · local-first
Nothing leaves the house
Flows are files in a folder on your machine. No account, no signup, no new data processor.
For your manager: no new vendor, no new data processor, nothing leaves the house.
Database passwords, Jira tokens and model keys live in your context folder. They are never sent to the browser.
Version flows in your own repository, review them in your own PRs.
Seeded examples run with no network at all.
A local Ollama model keeps even flow writing on your machine.
Licensing
MIT. Free. Full stop.
Read the source, fork it, keep it forever. No per-seat licence, no vendor, no paid tier. Installing needs no account and no email.
LICENSE · MITLimits
What it is not
nota unit test framework. It tests behaviour across systems, not functions.
nota load testing tool. One flow, one run, one verdict.
betabrowser automation through Playwright is experimental. Expect rough edges.
readthe Xray integration never writes to Jira. Pull only.
Quick start
Your first flow runs in five minutes
The first run seeds example applications and flows that work offline. Open the notebook, press Run, and start replacing the examples with your systems. Node and npm are the only prerequisites.
$ npm install -g ronsel $ ronsel --server → http://localhost:3001
Who builds it
lab34, Galicia, Spain
lab34 is an engineering company building AI for IT operations. ronsel is open source — published because working tools are better proof than slides. More at lab34.es.
“Working tools are better proof than slides. ronsel is the tool we wanted when a bug lived between three systems and nobody could read the test that should have caught it.”
Jose Constela · founder, lab34