Skip to content

Commit 118587b

Browse files
committed
tests/interaction: add era-axis types to the requirements manifest
Adds the type machinery for parametrizing the interaction suite over (transport, spec_version) cells, with no behaviour change yet: - SpecVersion / SPEC_VERSIONS / CONNECTABLE_TRANSPORTS / TRANSPORT_SPEC_VERSIONS constants; SPEC_REVISION now derived from SPEC_VERSIONS[-1] - ArmExclusionReason literal + ArmExclusion / KnownFailure dataclasses - Requirement gains note / added_in / removed_in / supersedes / superseded_by / arm_exclusions / known_failures (all defaulted, no manifest entry edited) - Connect protocol + the three connect_* factories accept a protocol_version kwarg (currently ignored) 534 tests collected (unchanged); pyright/ruff clean. Claude-Session: https://claude.ai/code/session_01NF95tmzF6RhVPktJdL6fK5
1 parent 734746a commit 118587b

2 files changed

Lines changed: 84 additions & 1 deletion

File tree

tests/interaction/_connect.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __call__(
6969
message_handler: MessageHandlerFnT | None = None,
7070
client_info: Implementation | None = None,
7171
elicitation_callback: ElicitationFnT | None = None,
72+
protocol_version: str = LATEST_PROTOCOL_VERSION,
7273
) -> AbstractAsyncContextManager[Client]: ...
7374

7475

@@ -83,6 +84,7 @@ async def connect_in_memory(
8384
message_handler: MessageHandlerFnT | None = None,
8485
client_info: Implementation | None = None,
8586
elicitation_callback: ElicitationFnT | None = None,
87+
protocol_version: str = LATEST_PROTOCOL_VERSION,
8688
) -> AsyncIterator[Client]:
8789
"""Yield a Client connected to the server over the in-memory transport."""
8890
async with Client(
@@ -113,6 +115,7 @@ async def connect_over_streamable_http(
113115
message_handler: MessageHandlerFnT | None = None,
114116
client_info: Implementation | None = None,
115117
elicitation_callback: ElicitationFnT | None = None,
118+
protocol_version: str = LATEST_PROTOCOL_VERSION,
116119
) -> AsyncIterator[Client]:
117120
"""Yield a Client connected to the server's streamable HTTP app, entirely in process.
118121
@@ -326,6 +329,7 @@ async def connect_over_sse(
326329
message_handler: MessageHandlerFnT | None = None,
327330
client_info: Implementation | None = None,
328331
elicitation_callback: ElicitationFnT | None = None,
332+
protocol_version: str = LATEST_PROTOCOL_VERSION,
329333
) -> AsyncIterator[Client]:
330334
"""Yield a Client connected to the server's legacy SSE transport, entirely in process."""
331335
app, _ = build_sse_app(server)

tests/interaction/_requirements.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,41 @@
3939

4040
import pytest
4141

42-
SPEC_REVISION = "2025-11-25"
42+
from mcp.shared.version import KNOWN_PROTOCOL_VERSIONS
43+
44+
SpecVersion = Literal["2025-11-25", "2026-07-28"]
45+
"""A protocol version the suite parametrizes over. Both values are typed even though only one is
46+
on the active axis (SPEC_VERSIONS) until the 2026-07-28 implementation lands."""
47+
48+
SPEC_VERSIONS: tuple[SpecVersion, ...] = ("2025-11-25",)
49+
"""The active spec-version matrix axis, ordered oldest to newest. Every entry must be in KNOWN_PROTOCOL_VERSIONS."""
50+
51+
SPEC_REVISION = SPEC_VERSIONS[-1]
4352
SPEC_BASE_URL = f"https://modelcontextprotocol.io/specification/{SPEC_REVISION}"
4453

4554
Transport = Literal["in-memory", "stdio", "streamable-http", "sse"]
4655

56+
CONNECTABLE_TRANSPORTS: tuple[Transport, ...] = ("in-memory", "sse", "streamable-http")
57+
"""Transports the connect fixture fans out over (the subset with a factory in conftest._FACTORIES)."""
58+
59+
TRANSPORT_SPEC_VERSIONS: dict[Transport, tuple[SpecVersion, ...]] = {
60+
"sse": ("2025-11-25",),
61+
}
62+
"""Transports that only serve a subset of SPEC_VERSIONS. Absent => serves all. Consulted by compute_cells()."""
63+
64+
ArmExclusionReason = Literal[
65+
"asserts-legacy-handshake",
66+
"method-not-in-modern-registry",
67+
"legacy-only-vocabulary",
68+
"modern-error-surface",
69+
"requires-session",
70+
"drives-transport-directly",
71+
"server-initiated-request",
72+
]
73+
"""Machine-readable reasons a requirement is excluded from a (transport, spec_version) matrix cell.
74+
The set doubles as a re-admission checklist: when a feature lands, grep for its reason to find the
75+
cells to re-admit. Values are kept byte-identical to the typescript-sdk's EntryExclusionReason."""
76+
4777
_TestFn = TypeVar("_TestFn", bound=Callable[..., object])
4878

4979
_SOURCE_PATTERN = re.compile(r"https://modelcontextprotocol\.io/specification/.+|sdk|issue:#\d+")
@@ -63,6 +93,38 @@ class Divergence:
6393
issue: str | None = None
6494

6595

96+
@dataclass(frozen=True, kw_only=True)
97+
class ArmExclusion:
98+
"""Excludes a requirement from a (transport, spec_version) matrix cell, with a typed reason."""
99+
100+
reason: ArmExclusionReason
101+
transport: Transport | None = None
102+
spec_version: SpecVersion | None = None
103+
note: str | None = None
104+
105+
def __post_init__(self) -> None:
106+
if self.spec_version is not None and self.spec_version not in KNOWN_PROTOCOL_VERSIONS:
107+
raise ValueError(f"spec_version {self.spec_version!r} is not in KNOWN_PROTOCOL_VERSIONS")
108+
109+
110+
@dataclass(frozen=True, kw_only=True)
111+
class KnownFailure:
112+
"""A (transport, spec_version) cell where the requirement's test is expected to fail (strict xfail)."""
113+
114+
note: str
115+
transport: Transport | None = None
116+
spec_version: SpecVersion | None = None
117+
issue: str | None = None
118+
119+
def __post_init__(self) -> None:
120+
if not self.note.strip():
121+
raise ValueError("note must be non-empty")
122+
if self.spec_version is not None and self.spec_version not in KNOWN_PROTOCOL_VERSIONS:
123+
raise ValueError(f"spec_version {self.spec_version!r} is not in KNOWN_PROTOCOL_VERSIONS")
124+
if self.issue is not None and not re.fullmatch(r"#\d+|https://github\.com/\S+", self.issue):
125+
raise ValueError(f"issue must be '#<n>' or a GitHub URL, got {self.issue!r}")
126+
127+
66128
@dataclass(frozen=True, kw_only=True)
67129
class Requirement:
68130
"""A single testable behaviour and the provenance of why it must hold."""
@@ -73,10 +135,27 @@ class Requirement:
73135
divergence: Divergence | None = None
74136
deferred: str | None = None
75137
issue: str | None = None
138+
note: str | None = None
139+
added_in: SpecVersion | None = None
140+
removed_in: SpecVersion | None = None
141+
supersedes: tuple[str, ...] = ()
142+
superseded_by: str | None = None
143+
arm_exclusions: tuple[ArmExclusion, ...] = ()
144+
known_failures: tuple[KnownFailure, ...] = ()
76145

77146
def __post_init__(self) -> None:
78147
if not _SOURCE_PATTERN.fullmatch(self.source):
79148
raise ValueError(f"source must be a specification URL, 'sdk', or 'issue:#n', got {self.source!r}")
149+
if self.added_in is not None and self.added_in not in KNOWN_PROTOCOL_VERSIONS:
150+
raise ValueError(f"added_in {self.added_in!r} is not in KNOWN_PROTOCOL_VERSIONS")
151+
if self.removed_in is not None and self.removed_in not in KNOWN_PROTOCOL_VERSIONS:
152+
raise ValueError(f"removed_in {self.removed_in!r} is not in KNOWN_PROTOCOL_VERSIONS")
153+
if (
154+
self.added_in is not None
155+
and self.removed_in is not None
156+
and KNOWN_PROTOCOL_VERSIONS.index(self.added_in) >= KNOWN_PROTOCOL_VERSIONS.index(self.removed_in)
157+
):
158+
raise ValueError(f"added_in {self.added_in!r} must be earlier than removed_in {self.removed_in!r}")
80159

81160

82161
REQUIREMENTS: dict[str, Requirement] = {

0 commit comments

Comments
 (0)