0004: Reference self-tests¶
Status¶
Accepted for 0.10.0.
Problem¶
A scenario can fail because the agent regressed, but it can also fail because the harness is miswired or the gate is wrong. Ordinary agent runs cannot prove that a declared gate recognizes a known-correct trajectory or rejects a known defect. Wind Tunnel therefore needs tests for the test itself.
A reference self-test has two complementary cases:
- a golden case scripts known-correct model decisions and must pass the scenario's declared gate;
- a poison case scripts a named known-bad decision path and must fail that gate.
The script replaces model inference only. The runtime's real agent loop, tool mounting, fixture mutations, MCP evidence, state probes, preconditions, trace construction, and scoring remain live. A fabricated final trace would test a scorer in isolation; it would not certify the plumbing that produces the evidence the scorer consumes.
Public authoring contract¶
Reference cases belong to Scenario, alongside the gate they certify:
from windtunnel.api import (
ReferenceCase,
ReferenceDecision,
ReferenceToolCall,
Scenario,
)
scenario = Scenario(
name="artifact_guard",
prompt="Create the safe artifact.",
target_facts=[["artifact complete"]],
requires_tool_use=True,
reference_cases=[
ReferenceCase(
name="known-good",
kind="golden",
decisions=(
ReferenceDecision(tool_calls=(
ReferenceToolCall("write_artifact", {"safe": True}),
)),
ReferenceDecision(content="artifact complete"),
),
),
ReferenceCase(
name="unsafe-write",
kind="poison",
decisions=(
ReferenceDecision(tool_calls=(
ReferenceToolCall("write_artifact", {"safe": False}),
)),
ReferenceDecision(content="artifact complete"),
),
),
],
)
decisions are successive model responses inside one agent turn. Every
non-final decision has at least one tool call. The final decision has non-empty
content and no tool calls. Tool arguments must be JSON-serializable. Case names
must be unique within a scenario.
Reference data is source-controlled test data. Use fictional fixtures and never embed production prompts, credentials, customer records, or private incident payloads.
Optional runtime capability¶
An ordinary AgentRuntime remains valid. A runtime opts into self-testing by
also satisfying ReferenceCapableAgentRuntime:
from windtunnel.spi import AgentConfig, ReferenceCase
class MyRuntime:
def provision(self, config: AgentConfig, mcps=None):
...
def provision_reference(self, config: AgentConfig, case: ReferenceCase, mcps=None):
self.inference_substitute.install(case.decisions)
return self._provision_normal_agent(config, mcps)
The capability is declared once per runtime service, not configured per
scenario. The driver owns the location and mechanism of its inference seam;
Wind Tunnel does not model URLs, proxies, provider protocols, or service
topology. provision_reference() must return an otherwise normal
AgentHandle, and handle.teardown() must restore or remove the substitution.
The built-in in_memory runtime intentionally does not implement this
capability. It shortcuts the real agent/tool loop, so reporting it as a
reference-capable runtime would certify a different system.
Execution and isolation¶
wt selftest uses normal runtime and pack discovery, then executes this order:
- Resolve the runtime plugin and build the runtime once.
- Discover and select scenario packs with the same
--scenario,--tag,--pack, and--ownersemantics aswt run. - Verify that the runtime exposes
provision_reference(). An unsupported runtime produces explicitUNSUPPORTEDcase records and does not prepare fixtures or call pack factories. - Call the plugin's optional
pre_run(runtime, scenarios, runtime_name)once. - For each reference case, read the selected scenario's owning pack and create fresh MCP/probe wiring.
- Call
provision_reference()once for that case, then run the ordinaryrun_scenario()path with one run. - Tear down the handle before starting the next case.
Fresh provisioning and fresh pack factories are required per case. A golden case must not seed state that makes a poison case pass or fail, and one poison must not contaminate another.
State-probe visibility timing¶
StateProbeAvailable() inspects PreconditionContext.state_probe; it does not
discover probes hidden inside plugins, scorers, or runtime internals. In the
CLI path, that context is populated only by this sequence:
RuntimePlugin.pre_run()may install or replace the selected owning pack'sstate_probe_factoryafter its live fixture is ready.wt selftestcalls that factory for the current case.- The returned probe is passed to
run_reference_case(..., state_probe=...). - The ordinary runner provisions the handle and evaluates preconditions.
A scorer-level "probe missing" guard can still protect scoring, but it does
not populate PreconditionContext.state_probe. Declare
StateProbeAvailable() only where the owning pack factory or a direct library
call guarantees that the probe exists before preconditions run.
Verdicts and exit codes¶
Self-test verdicts are deliberately separate from ordinary scenario scores:
| Verdict | Meaning |
|---|---|
PASS |
Golden passed its gate, or poison failed its gate. |
GOLDEN_FAILED |
Known-correct behavior did not pass the gate. |
POISON_PASSED |
Known-bad behavior escaped the gate. |
UNSUPPORTED |
Runtime does not expose the optional capability. |
ERROR |
Execution was invalid: precondition, integrity, setup, runtime, probe-capture, or MCP-evidence failure. |
A rejected poison has an ordinary failed Score and a self-test verdict of
PASS; collapsing those values would invert the result. JSON and JUnit output
therefore carry the self-test verdict explicitly while retaining the ordinary
score as evidence.
- exit
0: every selected reference case isPASS; - exit
1: any case isGOLDEN_FAILED,POISON_PASSED, orERROR; - exit
2: unsupported runtime, invalid CLI usage, or no selected references.
Every executed case writes the normal trace and score sidecar under
runs/selftest by default. It does not append to the ordinary run ledger or
change normal pass-rate aggregates.
wt selftest --runtime <reference-capable-runtime>
wt selftest --runtime <runtime> --pack synthetic \
--format junit --out selftest.xml
Non-goals¶
- Reference cases are not a substitute for stochastic real-model runs.
- Wind Tunnel does not prescribe an inference provider or transport.
- Core does not execute tool calls from the script; the live agent loop does.
- Self-test results do not rewrite scenario gates or bless API stability.