Per-request shape probe. A request carrying the verify option runs the route's actual response through ShapeMatcher against one or more scenarios declared in config. No synthetic input — the actual is always whatever the real request produced. The probe is for monitoring and debugging, not for batch testing; batch testing lives in --type test.
Scenarios attach to a route by name. The tree is scenarios.<route-name>.<scenario-name>.{expected, description, ...}. Only expected: is read by the probe; extra fields like description surface in failure payloads.
service: hello-world
port: 9294
boundary_path: lib/hello_world/boundaries
routes:
/hello:
method: get
boundary: echo
name: hello
/greet/:name:
method: get
boundary: greet
name: greet
scenarios:
hello:
smoke:
description: "echoes the message param"
expected:
echoed: "world"
greet:
alice:
description: "canonical greeting format"
expected:
greeting: "Hello, alice!"
polite_tone:
description: "greeting always starts with Hello"
expected:
greeting:
matches: "^Hello, "
!Fixture-loadedExternal files keep long scenarios out of config.yml and let multiple routes share expectations. Configure the Fixture resolver with search dirs:
resolvers:
Fixture:
fixture_dirs:
- scenarios
- fixtures
scenarios:
greet:
alice: !Fixture greet/alice
polite_tone: !Fixture greet/polite_tone
regression_ticket_742: !Fixture bugs/ticket-742-greeting-regression
hello:
smoke: !Fixture hello/smoke
Each !Fixture <name> looks up <dir>/<name>.yml (and <dir>/mocks/<name>.yml) across the configured fixture_dirs; first hit wins. The loaded YAML replaces the tag at parse time, so after resolution scenarios.greet.alice is a plain hash with expected: and whatever else the fixture file carries.
A fixture file (scenarios/greet/alice.yml):
description: "canonical greeting format"
expected:
greeting: "Hello, alice!"
A bug-regression fixture (scenarios/bugs/ticket-742-greeting-regression.yml):
description: |
Ticket #742 — greeting lost its comma in release 0.4.2.
Guards against "Hello alice" sneaking past in a future refactor.
expected:
greeting:
matches: "^Hello, [a-zA-Z]+!$"
Fixtures and inline entries can mix inside the same scenarios: block.
Two equivalent forms map to the same verify option:
X-Wanderland-Verify: <scenario-name> — HTTP header--wanderland-verify=<scenario-name> — CLI flagcontext["options"]["verify"] — value projection read by the boundaryThe option value is always a string. Two conventions:
<scenario-name> — run that single scenario's expected against the actual response."route" — run every scenario configured for the current route. All must pass.Missing or empty option = verify boundary skips. Zero cost when you're not probing.
The probe returns a passthrough signal carrying the original payload plus _verify — one entry per scenario run. The prior crossing's type_addr is preserved, so format and seal handle it as any other success.
GET /greet/alice HTTP/1.1
X-Wanderland-Verify: alice
200 OK
{
"greeting": "Hello, alice!",
"_verify": [
{
"name": "alice",
"description": "canonical greeting format",
"passed": true,
"failures": []
}
]
}
The probe halts with 422, naming the failing scenarios and the ShapeMatcher failures each one produced. The request fails visibly — a probe that silently passed a wrong response would defeat the point.
GET /greet/bob HTTP/1.1
X-Wanderland-Verify: alice
422 Unprocessable Entity
{
"error": "scenario mismatch",
"route": "greet",
"verify": "alice",
"scenarios": [
{
"name": "alice",
"description": "canonical greeting format",
"passed": false,
"failures": ["greeting: expected \"Hello, alice!\", got \"Hello, bob!\""]
}
]
}
X-Wanderland-Verify: route runs every scenario attached to the current route. The passthrough payload's _verify carries them all; a halt lists only the failing ones.
GET /greet/bob HTTP/1.1
X-Wanderland-Verify: route
422 Unprocessable Entity
{
"error": "scenario mismatch",
"route": "greet",
"verify": "route",
"scenarios": [
{
"name": "alice",
"description": "canonical greeting format",
"passed": false,
"failures": ["greeting: expected \"Hello, alice!\", got \"Hello, bob!\""]
}
]
}
Asking to verify a route that has no scenarios — or naming a scenario that isn't registered — halts with 422 and the list of what's available:
GET /greet/alice HTTP/1.1
X-Wanderland-Verify: typo_name
422 Unprocessable Entity
{
"error": "no scenarios found",
"route": "greet",
"verify": "typo_name",
"available": ["alice", "polite_tone", "regression_ticket_742"]
}
Declared in core_injections at position: last, between enforce_denials (interleave) and trace_emit (last). Because position: last appends at fold time, the earlier declaration wins the earlier spot in the tail:
user slots
↓
enforce_denials (interleaved between user slots)
↓
verify_route ← runs the shape probe
↓
trace_emit ← attaches _trace if requested
↓
format ← MIME-matched renderer
↓
seal ← root commitment over the chain
Sitting before trace_emit means the verify crossing is part of the trace when --wanderland-trace is also set — the trace shows both what the route produced and how it measured against the scenario.
--wanderland-tracecurl -H 'X-Wanderland-Verify: route' \
-H 'X-Wanderland-Trace: 1' \
http://localhost:9294/greet/alice
Response payload includes _verify (the scenario outcomes) and _trace (the event stack, which itself includes the verify_route crossing and its result). Useful for debugging exactly where a response shape drifted.
--type test — The batch runner that synthesises inputs for each scenario, walks every route, and reports across all of them. Separate archetype; no overlap with the request-time probe. The same scenarios: block feeds both.Wanderland::Scenario.rspec_describe!(runtime) emits one rspec example per scenario using the same Scenario#verify(runtime:) that this probe reads via the header. Dev inner loop; same comparison, different surface.expected: — matches, contains, keys, has_key, first, any, count, gte/lte, not, empty — is the full ShapeMatcher surface.wanderland-core/lib/wanderland/boundaries/verify_route.rb — the boundarywanderland-core/lib/wanderland/option.rb — :verify option registrationwanderland-core/lib/wanderland/archetypes/engine.yml — core_injections declarationwanderland.dev