peira (peer-uh) · ai-native api testing
Your test plan compiles.
A deterministic runner executes it.
Intent compiler, deterministic runner, failure triage, and evidence ledger in a single CLI. Point peira compile and peira run at any REST API.
No API key · No signup · Zero LLM at runtime · MIT
run-start · the loop
How it works
AI-native testing is not old testing with a model bolted on — the division of labor changes. Follow one promise through the whole loop, artifacts included.
what you write
## Cancelling a shipped order Cancelling an order that has already shipped is refused with 409, and the order stays SHIPPED.
…that's the whole authoring surface — your agent handles the tagging.
what you get
- an executable, reviewable case — 16 sections became 26 cases in the reference bed
- 5 fresh seeded probes per run for every invariant section
- Given/When/Then docs and a visual HTML run report, regenerated on demand
- a trust ledger where this section earns applied, run by run
You describe the promise — plain English
Your whole job is the sentences. The tag is mechanical: your agent adds it (it's in the drop-in agent instructions), peira adopt adds it to existing documents, and untagged headings work anyway — ids derive from the text. The id just makes lineage survive rewording.
Human · agent tagsintent/orders.md
## Cancelling a shipped order ← you <!-- peira: id=order-cancel-shipped kind=ac --> ← your agent Cancelling an order that has already shipped ← you is refused with 409, and the order stays SHIPPED.
Humans hand-write test code
Humans write intent — plain markdown, one promise per section
Test code silently drifts from the spec
Every case records the intent it came from; when that text changes, the case is flagged and recompiled
Sleeps, retries, and flaky green
Seeded, deterministic verdicts — any failure replays exactly
A red build is a chore to diagnose
Failures come back triaged — bug | drift | flake — with evidence
peira render
Readable output
Cases are JSON with subset-matching semantics. One command turns the same case into Given/When/Then — or a full visual HTML run report with the observed request/response log attached to every failure.
GivenPOST /orders as alice — captures orderId ← body.id
WhenGET /orders/status?id=$orderId, polling until { status: "CONFIRMED" }
Thenthe response is 200, and the body matches { id: $orderId, status: "CONFIRMED" }
Finallyevery captured job is drained to a terminal state
From intent status-visibility @ ae5ab7a63816 — when the section's text changes, this case is flagged stale and recompiled.
--seed 42
Seeded test data
The seed is a run's replay number. Every “random” value is hash(seed, case id, key) — pure functions, nothing stored. Same seed, same service state → same verdicts. Any failure replays exactly.
peira triage
Failure triage
Infrastructure errors are routed away before the model sees anything — misreading an unreachable environment as a product bug is mechanically impossible. What remains is judged against the intent text, not the case.
The service broke its promise
Observed behavior contradicts what the intent requires. Comes back as a finding — expectation, observation — ready to file.
The promise moved
The case's encoding is violated while the intent's real requirements still hold. Comes back as the smallest intent-level diff — you approve or reject it.
The evidence is thin
A failure pattern suggesting nondeterminism. Comes back as a re-run prescription — with the seed, so the replay is exact.
case-verdict → applied | contradicted
Evidence ledger
Record an adjudicated run with peira evidence; read the standings with peira trust. A passing section logs applied — and so does a triaged bug, because the section did its job catching the violation. Adjudicated drift logs contradicted, with the reason quoted verbatim. Unadjudicated noise logs nothing.
| section | applied | contradicted | runs | last applied |
|---|---|---|---|---|
| plan#parallel-request-queueing | 3 | 0 | 3 | 2026-08-29 |
| plan#robustness | 3 | 0 | 3 | 2026-08-29 |
| plan#get-status | 2 | 1 | 2 | 2026-08-29 |
| plan#post-submit | 2 | 1 | 2 | 2026-08-29 |
real rows — peira’s own committed ledger, 3 recorded runs
evidence · docs/findings/
Metrics
The bed in this repo re-implements a service as a test fixture. The 27 hand-written specs it ships with are the originals — written years earlier, for the service it re-implements.
LLM calls at runtime
0
CI needs no API key, no session — the exit code gates the merge
legacy specs re-expressed
27/27
the original hand-written suite, re-expressed in request, capture and expect — without dropping to custom code once, and with no sleeps
divergences surfaced
3
compiling its legacy test plan verbatim caught intent/implementation gaps — one never tested by anyone
external API, first try
5/5
compiled against a public third-party API; the bed config was one URL
triage agreement
87.9%
bug-vs-drift across 33 behavior changes planted in advance; zero schema refusals or injections
tests on the tool itself
316
run against the exact compiled artifact that ships — strict TypeScript, Linux and Windows in CI
to the first verdict
57ms
boot, load and validate the whole suite, issue a real request, report — no transform pipeline, no JVM, no browser
tool overhead per case
0.17ms
measured, not claimed (npm run bench) — what you wait for is your service, not the runner
run-end · 0 error
Get started
One markdown file and one URL is enough to start. The first run writes evidence; the third run starts earning trust.
No API key · No signup · Zero LLM at runtime · MIT
Read the docs — getting started, CLI reference, case anatomy