Third adapter alongside http and cli. Walks the scenarios: block in config, synthesises a request per scenario through normal dispatch with X-Wanderland-Verify set, and interprets the returned crossing. Exits 0 when every matched scenario passes, non-zero otherwise.
Same binary, same config, same chain. The only thing test mode adds on top of the verify_route probe is iteration and an exit code — the shape comparison is still the verify_route boundary doing its one job.
wanderland --type test config.yml
wanderland --type test config.yml --route greet
wanderland --type test config.yml --scenario alice
wanderland --type test config.yml --route greet --scenario alice
Arguments:
<config.yml> — required, the same config that boots --type http and --type cli--route NAME — only run scenarios attached to this route--scenario NAME — only run scenarios with this name (across all routes)Filters combine with AND semantics. An unknown filter reports the configured routes with scenarios and exits non-zero.
Live probes via X-Wanderland-Verify only need expected: — the actual comes from the real request. Test mode synthesises the request, so scenarios need both input: and expected::
scenarios:
greet:
alice:
description: "Canonical greeting format"
input:
params:
name: alice
expected:
greeting: "Hello, alice!"
default_output:
description: "Empty name defaults to world"
input:
params:
name:
expected:
greeting: "Hello, world!"
polite_tone:
description: "Greeting always starts with Hello"
input:
params:
name: bob
expected:
greeting:
matches: "^Hello, "
input.params is the merged params hash — path captures, query, and body folded into one. Path captures expand through the route pattern when the runner resolves the synthetic path: /greet/:name with params.name: alice becomes /greet/alice in the dispatched request.
Optional input.headers attaches custom request headers; the runner adds X-Wanderland-Verify: <scenario-name> on top so the verify_route boundary fires.
A scenario missing input: runs with empty params. Whatever the route's default behaviour is, that's the "actual" the expected shape matches against.
greet
✓ alice — Canonical greeting format
✓ default_output — Empty name defaults to world
✓ polite_tone — Greeting always starts with Hello
3 scenarios, 3 passed, 0 failed
Failures list the ShapeMatcher output verbatim:
greet
✓ alice — Canonical greeting format
✗ polite_tone — Greeting always starts with Hello
greeting: expected to match /^Hello, /, got "Howdy alice!"
2 scenarios, 1 passed, 1 failed
for each scenario in config.scenarios.<route>.<name>:
compiled = runtime.engine.named_routes[route]
path = compiled.pattern.expand(params)
headers = input.headers.merge("X-Wanderland-Verify" => name)
crossing = Dispatch.invoke(runtime, compiled,
params:, headers:, path:, adapter: "http")
if type_addr is :signals:stop:*
fail with result.scenarios[].failures
else
pass
The dispatch call exercises the whole chain — adapter announcement, denial enforcement, every user slot, verify_route, trace_emit, format, seal. The test runner watches the final crossing's type_addr: a blocking address means verify_route halted the chain because the shape didn't match. A non-blocking address means everything ran through cleanly.
Because the synthetic request carries a real verify header, the existing verify_route boundary does the comparison. No duplication of the match logic in the test runner.
One scenarios: block, three ways to run it:
| Lens | Trigger | Input source | Exit signal |
|---|---|---|---|
--type test |
wanderland --type test config.yml |
scenario.input (synthetic) |
exit code |
| HTTP probe | X-Wanderland-Verify: <name> |
real request | 422 on mismatch |
| RSpec | Wanderland::Scenario.rspec_describe!(runtime) |
scenario.input (synthetic) |
expect(failures).to be_empty |
Dev exercises scenarios under rspec. CI runs --type test against the same config. Staging and prod attach probes via the verify header for synthetic monitoring. The scenarios don't care which lens is reading them.
0 — every matched scenario passed1 — one or more failed, or no scenarios matched the filters2 — invalid CLI argumentsWanderland::Scenario.rspec_describe!(runtime) hits the same Scenario#verify(runtime:) primitive this runner does; rspec wraps it in example blocks for the dev inner loop.wanderland-core/lib/wanderland/runners/test.rb — the runnerwanderland-core/bin/wanderland — --type test dispatchwanderland.dev