Execution¶
Added in v0.2.
By default, teststop run generates adversarial scenarios and validates them structurally. Point it at a running system with --target and teststop will also execute those scenarios and feed the real pass/fail outcomes into confidence memory.
This is the jump from a scenario generator to a scenario runner.
teststop tests what's running — it doesn't run your app¶
teststop never starts, builds, or manages the system under test. You run your app however you like — a local process, a container, a staging deployment — and point --target at it:
This is deliberate. teststop is a thin trigger; managing your app's lifecycle would make it "the new loop that needs maintaining," which the project explicitly avoids. The upside: you can target any environment, including a production-like instance, and teststop adapts to it.
Hybrid execution¶
teststop chooses how to execute each scenario individually:
flowchart TD
A[Scenario] --> B{exec block<br/>and --target set?}
B -- yes --> C[HTTP executor<br/>deterministic net/http]
B -- no --> D{--target set?}
D -- yes --> E[AI executor<br/>AI performs the steps]
D -- no --> F[Static executor<br/>structural validation] | Condition | Executor | Behavior |
|---|---|---|
Scenario has a structured exec block and --target is set | HTTP | Fires the exact request with net/http; judged on status code |
--target set, scenario is prose-only | AI-driven | The AI actually performs the steps against the target and returns a verdict |
No --target | Static | Validates scenario structure only (default) |
The HTTP path is deterministic and fast (no AI call at execution time). The AI path covers open-ended, chaos-heavy scenarios that can't be reduced to one request. See the exec field for the structured contract.
Predicted vs executed¶
This distinction matters when you read the results:
- Predicted (no
--target) — teststop generates scenarios and validates them structurally. Nothing is sent to a system. The report is a risk surface, and the confidence is labelled PREDICTED — it is not evidence of correctness. - Executed (with
--target) — scenarios are run and judged on real responses. Failures are observed, not guessed, and confidence reflects verified behavior.
The JSON output makes this explicit via exec_summary.executed. Treat a predicted run as a checklist of "things that could break a system like this," not a defect count — run with --target to verify which (if any) actually break.
Concurrency races¶
A single request can't prove a system is safe against double-submit or claim-the-last-item races. Set concurrency in the exec block and teststop fires N identical requests simultaneously, asserting the guard lets at most one winner through:
{
"exec": {
"mode": "http",
"method": "POST",
"path": "/actions/42/approve",
"expected_status": 200,
"concurrency": 10
}
}
A winner is any 2xx response (a request that actually succeeded). The bug teststop detects is more than one winner — the guard let concurrent duplicates mutate state.
- Pass — at most one request wins (a
2xx) and the rest are cleanly rejected with a4xx(e.g.409). Zero winners is also safe (nothing was mutated — e.g. an auth endpoint that correctly rejects every concurrent attempt). - Fail — more than one request wins (the race is not guarded — the real bug), any request returns a
5xx, or a request fails to complete (transport error).
expected_status should be the winner's 2xx code; it is not used to classify winners, so a rejection code never counts as a success. actual_behavior reports the histogram, e.g. 10 concurrent POST …: 1×200, 9×409.
Limitation: state setup
Race mode fires N identical requests against the target's current state. It is ideal for guards that don't need per-request setup (create-with-unique-key, claim-last-item). Scenarios that must first seed state (approve an action that must already be pending) need a setup phase that teststop does not yet provide.
Flags¶
| Flag | Default | Description |
|---|---|---|
--target <url> | (none) | Base URL of the running system. Empty = static only. |
--concurrency <n> | 4 | Max scenarios executed in parallel |
--exec-timeout <dur> | 10s | Per-request timeout (15s, 500ms, …) |
--max-retries <n> | 2 | Retries for transport errors and 5xx responses |
Execution runs concurrently with a bounded worker pool; results are returned in scenario order.
Sandbox and localhost¶
teststop runs the AI CLI inside an Apple Container sandbox when available. A sandboxed container cannot reach your host's localhost, so:
# Local target → run the AI tester directly
TESTSTOP_SANDBOX=none teststop run --target http://localhost:8080
# Remote / staging target → sandbox works fine
TESTSTOP_SANDBOX=required teststop run --target https://staging.example.com
The deterministic HTTP executor always runs host-side, so the sandbox setting does not affect HTTP-mode scenarios.
Future work
Wiring the executor to run inside the sandbox's network (so even local execution is fully isolated) is tracked as a follow-up. Today, use TESTSTOP_SANDBOX=none for local targets.
What execution looks like¶
Running against a small Go API:
TESTSTOP_SANDBOX=none teststop run --path ./sample-api \
--target http://localhost:8099 --depth light --output json
{
"exec_summary": { "executed": true, "count": 5, "passed": 4, "failed": 1, "target": "http://localhost:8099" },
"executions": [
{
"scenario_id": "login-empty-whitespace-username",
"area": "api/login",
"mode": "http",
"passed": false,
"actual_behavior": "HTTP 401 in 5ms",
"failure_reason": "expected status 400, got 401",
"priority": "medium",
"duration_ms": 5
}
]
}
Here teststop found a real issue: a login with a whitespace-only username was authenticated (401) instead of being rejected as invalid input (400). That failure lowers confidence for the api/login area and is reflected in the exit code.
Effect on confidence and exit codes¶
- Each executed scenario updates its
confidence_area: a pass raises confidence, a failure drops it (see Memory). - A failed
criticalscenario sets exit code2(do not deploy). - Below-threshold average confidence sets exit code
1(review).
Skipped scenarios (infrastructure errors)¶
If the AI driver can't return a verdict for an infrastructure reason — the AI CLI exits with an error, hits a rate limit, or returns unparseable output — the scenario is marked skipped, not failed. Skipped scenarios:
- do not move confidence in either direction,
- are not counted as failures and never trigger exit code
2, - are surfaced separately in the report and as
exec_summary.skipped/executions[].skippedso you can see them without them polluting the signal.
This keeps a rate-limited run from fabricating "failures" that say nothing about the system under test. The report shows e.g. Results: 19 passed, 7 failed, 17 skipped (infrastructure error — not counted).
Without --target, well-formed scenarios pass structural validation — static-only mode.