Skip to content

Commit 66aa908

Browse files
test(conformance): capability-agnostic harness helpers for wire + carries
Adds tests/conformance/harness/wire.py with helpers used by structured- output and content-block fixtures (and any future capability fixtures that need the same shapes): - match_wire_body(actual, expected) — recursive deep-equal with "*" wildcard support for string slots. - assert_response_format_absent(body) — asserts the wire body has no response_format key. - assert_system_references_schema(body, schema) — asserts the first message in the body is a system message whose content contains the canonical-JSON form of the schema as a substring. - assert_error_carries(exc, carries) — introspects a raised exception's attributes against an expected_carries block; supports _present / _mentions / literal-equal forms; handles the raw_response_content → raw_content fixture-vs-impl naming alias. Extends test_llm_provider.py to drive these from the existing fixture loop: - response_schema is read from call_spec and threaded through provider.complete(). - expected_wire_request literal compare + expected_wire_request_checks sibling checks fire after each captured chat-completions request. - caller_messages_unmodified takes a model_dump snapshot pre-call and asserts byte-equality post-call. - expected.response.parsed is compared for equality. - expected.raises.carries is fed to assert_error_carries. - retry_middleware: block wraps the call in a default-classifier retry simulator (transient = TRANSIENT_CATEGORIES membership); the captured-request count provides provider_call_count. - mock_provider.capabilities.supports_native_response_format: false constructs the provider with force_prompt_augmentation_fallback=True. The 0016 structured-output fixtures (021–028) remain skipped at this commit. The next commit removes their skip markers.
1 parent 16fb3c4 commit 66aa908

3 files changed

Lines changed: 350 additions & 6 deletions

File tree

tests/conformance/harness/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@
2424
)
2525
from .loader import discover_fixtures, load_fixture
2626
from .skip import SkipReason
27+
from .wire import (
28+
assert_error_carries,
29+
assert_response_format_absent,
30+
assert_system_references_schema,
31+
match_wire_body,
32+
request_body,
33+
)
2734

2835
__all__ = [
2936
"CasesFixture",
3037
"Fixture",
3138
"GraphFixture",
3239
"LlmProviderFixture",
3340
"SkipReason",
41+
"assert_error_carries",
42+
"assert_response_format_absent",
43+
"assert_system_references_schema",
3444
"discover_fixtures",
3545
"load_fixture",
46+
"match_wire_body",
47+
"request_body",
3648
]

tests/conformance/harness/wire.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Generic helpers for conformance fixtures that assert on captured wire
2+
requests and on the attributes of raised exceptions.
3+
4+
These helpers are capability-agnostic: any fixture format that uses
5+
``expected_wire_request`` (literal compare with wildcards),
6+
``expected_wire_request_checks`` (sibling boolean checks), or
7+
``expected.raises.carries`` (error-attribute introspection) can drive
8+
into the same helpers.
9+
10+
The ``"*"`` literal in an ``expected_wire_request`` string slot is a
11+
wildcard: the actual value MUST be present and a non-empty string, but
12+
the specific value is exempted from literal comparison. This convention
13+
is documented in the spec's llm-provider conformance fixtures
14+
(021/026/027) and inherited by any future capability that needs the
15+
same shape.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import json
21+
from collections.abc import Mapping
22+
from typing import Any, cast
23+
24+
import httpx
25+
26+
WILDCARD = "*"
27+
28+
29+
def request_body(captured: httpx.Request) -> dict[str, Any]:
30+
"""Decode a captured httpx request's body as a JSON object."""
31+
parsed = json.loads(captured.content)
32+
if not isinstance(parsed, dict):
33+
raise AssertionError(f"wire body is not a JSON object: {parsed!r}")
34+
return cast("dict[str, Any]", parsed)
35+
36+
37+
def match_wire_body(
38+
actual: Any,
39+
expected: Any,
40+
*,
41+
path: str = "$",
42+
) -> None:
43+
"""Recursive deep-equal between an actual wire-body value and an
44+
expected shape. Strings equal to ``"*"`` in the expected value match
45+
any non-empty string in the actual value. Keys present in
46+
``expected`` MUST be present in ``actual`` and equal; keys present
47+
in ``actual`` but absent from ``expected`` are allowed.
48+
49+
Raises :class:`AssertionError` with a JSON-pointer-style path on
50+
mismatch.
51+
"""
52+
if isinstance(expected, str) and expected == WILDCARD:
53+
if not (isinstance(actual, str) and actual):
54+
raise AssertionError(
55+
f"wire mismatch at {path}: expected non-empty string (wildcard), got {actual!r}"
56+
)
57+
return
58+
59+
if isinstance(expected, Mapping):
60+
if not isinstance(actual, Mapping):
61+
raise AssertionError(f"wire mismatch at {path}: expected object, got {type(actual).__name__}")
62+
expected_map = cast("Mapping[str, Any]", expected)
63+
actual_map = cast("Mapping[str, Any]", actual)
64+
for key, exp_v in expected_map.items():
65+
if key not in actual_map:
66+
raise AssertionError(f"wire mismatch at {path}: missing key {key!r}")
67+
match_wire_body(actual_map[key], exp_v, path=f"{path}.{key}")
68+
return
69+
70+
if isinstance(expected, list):
71+
if not isinstance(actual, list):
72+
raise AssertionError(f"wire mismatch at {path}: expected list, got {type(actual).__name__}")
73+
expected_list = cast("list[Any]", expected)
74+
actual_list = cast("list[Any]", actual)
75+
if len(actual_list) != len(expected_list):
76+
raise AssertionError(
77+
f"wire mismatch at {path}: length differs "
78+
f"(actual={len(actual_list)}, expected={len(expected_list)})"
79+
)
80+
for idx, (a, e) in enumerate(zip(actual_list, expected_list, strict=True)):
81+
match_wire_body(a, e, path=f"{path}[{idx}]")
82+
return
83+
84+
if actual != expected:
85+
raise AssertionError(f"wire mismatch at {path}: actual={actual!r}, expected={expected!r}")
86+
87+
88+
def assert_response_format_absent(body: Mapping[str, Any]) -> None:
89+
"""Assert the wire body has no ``response_format`` key."""
90+
if "response_format" in body:
91+
raise AssertionError(
92+
f"wire check failed: response_format present (value={body['response_format']!r}), expected absent"
93+
)
94+
95+
96+
def assert_system_references_schema(body: Mapping[str, Any], schema: Mapping[str, Any]) -> None:
97+
"""Assert the first wire message is a system message whose content
98+
references the supplied JSON Schema (via substring match of the
99+
canonical-JSON form).
100+
"""
101+
messages = body.get("messages")
102+
if not isinstance(messages, list) or not messages:
103+
raise AssertionError(
104+
"wire check failed: expected a non-empty messages list to verify system-message presence"
105+
)
106+
first = cast("list[Any]", messages)[0]
107+
if not isinstance(first, dict):
108+
raise AssertionError(
109+
f"wire check failed: first message is not an object (got {first!r}), "
110+
"cannot verify schema-directive reference"
111+
)
112+
first_dict = cast("dict[str, Any]", first)
113+
if first_dict.get("role") != "system":
114+
raise AssertionError(
115+
f"wire check failed: first message is not system (got {first_dict!r}), "
116+
"cannot verify schema-directive reference"
117+
)
118+
content = first_dict.get("content")
119+
if not isinstance(content, str):
120+
raise AssertionError(
121+
f"wire check failed: system message content is not a string (got {type(content).__name__})"
122+
)
123+
schema_json = json.dumps(schema, sort_keys=True)
124+
if schema_json not in content:
125+
raise AssertionError(
126+
"wire check failed: system message content does not contain the serialized schema; "
127+
f"content={content!r}"
128+
)
129+
130+
131+
def assert_error_carries(exc: BaseException, carries: Mapping[str, Any]) -> None:
132+
"""Introspect attributes of a raised exception against an
133+
expected-carries block. Supported keys:
134+
135+
- ``<attribute>_present: true`` — attribute MUST be set to a
136+
truthy non-None value (e.g., ``response_schema_present``,
137+
``failure_description_present``).
138+
- ``<attribute>: <value>`` — attribute value equals the supplied
139+
value (e.g., ``raw_response_content: '...'``).
140+
- ``<attribute>_mentions: <substring>`` — string attribute value
141+
contains the supplied substring (e.g.,
142+
``failure_description_mentions: 'age'``).
143+
"""
144+
for key, expected in carries.items():
145+
if key.endswith("_present"):
146+
attr = key[: -len("_present")]
147+
actual = _get_carries_attr(exc, attr)
148+
if bool(expected) and (actual is None or actual == ""):
149+
raise AssertionError(f"carries check failed: expected {attr!r} to be present, got {actual!r}")
150+
if not bool(expected) and (actual is not None and actual != ""):
151+
raise AssertionError(f"carries check failed: expected {attr!r} to be absent, got {actual!r}")
152+
elif key.endswith("_mentions"):
153+
attr = key[: -len("_mentions")]
154+
actual = _get_carries_attr(exc, attr)
155+
if not isinstance(actual, str):
156+
raise AssertionError(
157+
f"carries check failed: {attr!r} is not a string (got {type(actual).__name__}); "
158+
f"cannot substring-match {expected!r}"
159+
)
160+
if expected not in actual:
161+
raise AssertionError(
162+
f"carries check failed: {attr!r}={actual!r} does not mention {expected!r}"
163+
)
164+
else:
165+
actual = _get_carries_attr(exc, key)
166+
if actual != expected:
167+
raise AssertionError(
168+
f"carries check failed: {key!r} actual={actual!r}, expected={expected!r}"
169+
)
170+
171+
172+
def _get_carries_attr(exc: BaseException, name: str) -> Any:
173+
# Allow fixture-naming-friendly aliases for the carries block. The
174+
# spec fixtures use ``raw_response_content`` (the wire-side label);
175+
# the Python exception class names its attribute ``raw_content``.
176+
aliases = {"raw_response_content": "raw_content"}
177+
canonical = aliases.get(name, name)
178+
return getattr(exc, canonical, None)

0 commit comments

Comments
 (0)