Skip to content

Commit 4ac17ef

Browse files
test(mcp-adapters): implement property-based assertions
1 parent 127c1f7 commit 4ac17ef

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Property-based assertion library for MCP adapter testing.
2+
3+
All assertions verify adapter contracts using fields.py as the reference
4+
through HostSpec metadata. No hardcoded field names — everything is derived.
5+
6+
Usage:
7+
>>> from tests.test_data.mcp_adapters.assertions import assert_only_supported_fields
8+
>>> assert_only_supported_fields(result, host_spec)
9+
"""
10+
11+
from typing import Any, Dict
12+
13+
from hatch.mcp_host_config.fields import EXCLUDED_ALWAYS
14+
15+
# Import locally to avoid circular; HostSpec is used as type hint only
16+
from tests.test_data.mcp_adapters.host_registry import HostSpec
17+
18+
19+
def assert_only_supported_fields(result: Dict[str, Any], host: HostSpec) -> None:
20+
"""Verify result contains only fields from fields.py for this host.
21+
22+
After field mapping, the result may contain host-native names (e.g.,
23+
Codex 'arguments' instead of 'args'). We account for this by also
24+
accepting mapped field names.
25+
26+
Args:
27+
result: Serialized adapter output
28+
host: HostSpec with metadata derived from fields.py
29+
"""
30+
result_fields = set(result.keys())
31+
# Build the set of allowed field names: supported + mapped target names
32+
allowed = set(host.supported_fields)
33+
for _universal, host_specific in host.field_mappings.items():
34+
allowed.add(host_specific)
35+
36+
unsupported = result_fields - allowed
37+
assert not unsupported, (
38+
f"[{host.host_name}] Unsupported fields in result: {sorted(unsupported)}. "
39+
f"Allowed: {sorted(allowed)}"
40+
)
41+
42+
43+
def assert_excluded_fields_absent(result: Dict[str, Any], host: HostSpec) -> None:
44+
"""Verify EXCLUDED_ALWAYS fields are not in result.
45+
46+
Args:
47+
result: Serialized adapter output
48+
host: HostSpec (used for error context)
49+
"""
50+
excluded_present = set(result.keys()) & EXCLUDED_ALWAYS
51+
assert (
52+
not excluded_present
53+
), f"[{host.host_name}] Excluded fields found in result: {sorted(excluded_present)}"
54+
55+
56+
def assert_transport_present(result: Dict[str, Any], host: HostSpec) -> None:
57+
"""Verify at least one transport field is present in result.
58+
59+
Args:
60+
result: Serialized adapter output
61+
host: HostSpec with transport fields derived from fields.py
62+
"""
63+
transport_fields = host.get_transport_fields()
64+
present = set(result.keys()) & transport_fields
65+
assert present, (
66+
f"[{host.host_name}] No transport field present in result. "
67+
f"Expected one of: {sorted(transport_fields)}"
68+
)
69+
70+
71+
def assert_transport_mutual_exclusion(result: Dict[str, Any], host: HostSpec) -> None:
72+
"""Verify exactly one transport field is present in result.
73+
74+
Args:
75+
result: Serialized adapter output
76+
host: HostSpec with transport fields derived from fields.py
77+
"""
78+
transport_fields = host.get_transport_fields()
79+
present = set(result.keys()) & transport_fields
80+
assert len(present) == 1, (
81+
f"[{host.host_name}] Expected exactly 1 transport, "
82+
f"got {len(present)}: {sorted(present)}"
83+
)
84+
85+
86+
def assert_field_mappings_applied(result: Dict[str, Any], host: HostSpec) -> None:
87+
"""Verify field mappings from fields.py were applied.
88+
89+
For hosts with field mappings (e.g., Codex), universal field names
90+
should NOT appear in the result — only the mapped names should.
91+
92+
Args:
93+
result: Serialized adapter output
94+
host: HostSpec with field_mappings derived from fields.py
95+
"""
96+
for universal, host_specific in host.field_mappings.items():
97+
if universal in result:
98+
assert False, (
99+
f"[{host.host_name}] Universal field '{universal}' should have been "
100+
f"mapped to '{host_specific}'"
101+
)
102+
103+
104+
def assert_tool_lists_coexist(result: Dict[str, Any], host: HostSpec) -> None:
105+
"""Verify both allowlist and denylist fields are present in result.
106+
107+
Only meaningful for hosts that support tool lists. Skips silently
108+
if the host has no tool list configuration.
109+
110+
Args:
111+
result: Serialized adapter output
112+
host: HostSpec with tool list config derived from fields.py
113+
"""
114+
tool_config = host.get_tool_list_config()
115+
if not tool_config:
116+
return
117+
118+
allowlist = tool_config["allowlist"]
119+
denylist = tool_config["denylist"]
120+
121+
assert (
122+
allowlist in result
123+
), f"[{host.host_name}] Allowlist field '{allowlist}' missing from result"
124+
assert (
125+
denylist in result
126+
), f"[{host.host_name}] Denylist field '{denylist}' missing from result"
127+
128+
129+
def assert_unsupported_field_absent(
130+
result: Dict[str, Any], host: HostSpec, field_name: str
131+
) -> None:
132+
"""Verify a specific unsupported field is not in result.
133+
134+
Args:
135+
result: Serialized adapter output
136+
host: HostSpec (used for error context)
137+
field_name: The unsupported field that should have been filtered
138+
"""
139+
assert field_name not in result, (
140+
f"[{host.host_name}] Unsupported field '{field_name}' should have been "
141+
f"filtered but is present in result"
142+
)

0 commit comments

Comments
 (0)