Add the conformance requires_capability audience gate - #269
Conversation
A fixture case may declare which adapter class it applies to; a case selecting the other arm is a recognized skip rather than a failure. Declares langfuse_bound_provider_detection in the conformance manifest, wires the gate into the Langfuse runner's three case paths, and rejects an unimplemented gate in the OTel runner. Both ways of excluding a case are deliberately quiet, so a fixture that ends up running nothing would otherwise pass green. The guard counts executions rather than inferring them from the exclusion map, which also covers cases that share a name, and each recognized skip is reported where a passing run still shows it.
There was a problem hiding this comment.
Pull request overview
Adds the conformance-adapter §5.5 requires_capability audience gate to the Python conformance harness so fixture cases can select the correct contract arm per adapter capability, while preventing “green but asserted nothing” vacuous passes via explicit non-vacuity guards and recognized-skip reporting. This sets up upcoming observability fixtures (157/158/159) that require adapter-specific capabilities (notably Langfuse bound TracerProvider detection).
Changes:
- Introduces a single-sourced adapter capability manifest (
[adapter_capabilities]inconformance.toml) and a shared gating helper (tests/conformance/harness/capabilities.py). - Wires the gate into the Langfuse observability runner (including the hand-built 134 path) and adds a fail-loud rejection for gates in the OTel runner.
- Adds unit + conformance wiring tests that inject
requires_capabilityinto existing activated fixtures to exercise the behavior before the spec pin bump.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_conformance_capability_gate.py | Unit tests for capability declaration enforcement, gate semantics, and non-vacuity guard behavior. |
| tests/conformance/harness/capabilities.py | New shared capability manifest reader, gate evaluator, recognized-skip reporter, and non-vacuity assertion helper. |
| tests/conformance/test_observability_langfuse.py | Applies the gate to Langfuse fixture execution paths and enforces non-vacuity after in-loop exclusions. |
| tests/conformance/test_observability.py | Rejects requires_capability in the OTel runner until that runner implements per-case gating. |
| tests/conformance/test_capability_gate_wiring.py | End-to-end wiring tests that monkeypatch fixture loads to inject gates and assert runners consult them correctly. |
| conformance.toml | Declares adapter capabilities (starting with langfuse_bound_provider_detection = true) for the harness to consult. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… internal Two review findings, both the failure this PR exists to prevent. A requires_capability that is not a mapping now raises. The empty list was the dangerous spelling: it is falsy, so it read as ungated and ran the case against an arm meant for a different adapter class with nothing going red. The type is checked before the falsy test, and the parameter is typed as object because the value comes from YAML. The manifest test no longer skips when the Langfuse SDK is installed but the internal the adapter probes has been renamed or removed. That is precisely the case the test exists to catch, so it raises; only an absent SDK is still a legitimate skip.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
tests/conformance/test_capability_gate_wiring.py:260
- This truthiness check recreates the malformed-falsy hole that
capability_skip_reason()guards against. For example,requires_capability: [],false, or""is silently omitted from the sweep, so a runner outside observability can ignore the directive without this guard failing. Treat non-null, non-mapping values as detected invalid gates while preserving an empty mapping as ungated.
if any(isinstance(c, dict) and cast("dict[str, Any]", c).get("requires_capability") for c in cases):
tests/conformance/test_observability.py:654
- This runner also silently accepts falsy malformed gates. A case with
requires_capability: []reaches the OTel assertions even though the Langfuse path rejects the same shape, potentially exercising the wrong contract arm. Validate the value as a mapping before deciding whether it is empty.
gated = [cast("str", c.get("name") or "<unnamed>") for c in cases if c.get("requires_capability")]
tests/conformance/test_observability_langfuse.py:555
- The wiring suite exercises the generic multi-case loop and the special 134 loop, but never injects a gate into this single-case branch. Since the pinned fixtures carry no gates, deleting this call would not fail any test. Add a single-case runner test for both matching and mismatched requirements.
gate = capability_skip_reason(spec.get("requires_capability"))
tests/conformance/harness/capabilities.py:57
- The non-boolean manifest guard has no negative-path coverage: the current manifest contains a real boolean, and the malformed-value test only exercises fixture requirements. A quoted
"false"declaration could stop being rejected without any test failing. Add a test using a temporary manifest and clearadapter_capabilities' cache around it.
if not isinstance(value, bool):
tests/conformance/test_observability.py:682
- The new wiring call is not covered end to end: the added OTel test invokes
_reject_unsupported_capability_gate()directly. Removing this line would therefore leave all tests green at the current ungated spec pin. Add a runner-level test that injects a gate into a supported OTel fixture and verifies the entry point rejects it.
_reject_unsupported_capability_gate(fixture_id, spec)
The sweep globbed from the submodule root, which holds no capability folders, so it matched nothing and would have reported no gated fixtures outside observability however many landed. A companion test already pinned the detector against a synthetic tree, and its passing is what made the wrong root look verified: the detector worked while the haystack was empty. So the sweep now also asserts its scan reached the corpus, by requiring the observability conformance directory to be among the scanned parents.
Adds the
requires_capabilityaudience gate from conformance-adapter §5.5 (proposal 0116), the first of the harness prerequisites for observability fixtures 157/158/159.Some contracts have arms only one kind of adapter can take. 0116's raise arm needs the adapter to be able to establish which
TracerProvidera Langfuse client is bound to, which is not portably guaranteed, so a fixture case declares its audience withrequires_capabilityand each adapter declares what it can do. A case selecting the other arm is a recognized skip rather than a failure.conformance.tomlgains an[adapter_capabilities]table declaringlangfuse_bound_provider_detection = true. The declaration lives there rather than in the harness so the published conformance record and the test gate are the same statement, read once. The section it cites arrives with the pin bump, so the table is declared ahead of the fixtures that read it rather than orphaned.Not passing green while asserting nothing
Both ways of excluding a case, a capability gate and the existing per-case deferral, are deliberately quiet. That is what makes an empty run invisible: a fixture whose cases were all excluded reports success having checked nothing, and the output is identical to one that asserted everything. So the exclusion paths carry explicit guards.
total - len(excluded)would miss cases that share a name, since those collapse into one map entry and make an empty run look partial.bool("false")isTrueand a quoted value would select the capable arm while the published record rendersfalse.The gate is wired into all three of the Langfuse runner's case paths, including the hand-built 134 runner that returns before the generic loop. The OTel runner implements no gate, so it rejects one loudly rather than ignoring it and asserting an arm meant for a different adapter class. A repo-wide sweep covers the runners that have neither, since
requires_capabilityis a general directive andCaseSpecallows extra keys, so a gate landing in an llm-provider or checkpointing fixture would parse cleanly and then be dropped on the floor.Testing
No fixture at the current v0.107.0 pin carries
requires_capability, so the wiring cannot be demonstrated by running the corpus. Rather than leave it unexercised until the pin bump, the tests inject a gate into a real activated fixture and assert on the cases that actually ran. The repo-wide sweep is likewise searching an empty haystack today, so its detector is pinned separately against a synthetic tree.Every guard here was mutation tested: each was broken deliberately and confirmed to go red. One fix did not survive that on the first pass. The duplicate-name defense was tested only against the helper, so the runner's own arithmetic went unchecked until a test drove a fixture whose cases share a name.
The manifest test asserts the declaration against the Langfuse SDK actually installed rather than against its own literal, which would only prove the file parses. An upgrade that renames the internal the adapter probes would otherwise demote every client to
undetectablewith nothing going red.2042 passed, 504 skipped.
Follow-ups
The provider-faithful Langfuse fake was pulled from this PR and will land with the directives that consume it, so each of its fidelity claims is exercised by an assertion as it is written. An adversarial review of the combined change found nine defects in it, none of which the test suite could have caught, because nothing imported it.
A fail-loud allowlist for case-level and
expectedkeys in the Langfuse runner is deferred to the PR that adds the 157/158 directives._OBSERVATION_DIRECTIVESguards observation-level keys precisely because anexpectedblock is a chain ofif key in expectedchecks, so an unimplemented key is silently skipped. There is no such guard one level up, and it needs to exist before those fixtures are activated.