chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
"""Enforces the contract between the requirements manifest and the test suite.
|
||||
|
||||
The contract runs in both directions: every non-deferred entry in :data:`REQUIREMENTS` must be
|
||||
exercised by at least one test, and every test in the suite must carry at least one
|
||||
`@requirement(...)` mark referencing a manifest entry. Deferral reasons that point at coverage
|
||||
elsewhere in the repo must point at paths that exist. Test modules are imported directly
|
||||
(rather than relying on pytest collection) so the check holds even when only this file is run.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import re
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
from mcp_types import LATEST_PROTOCOL_VERSION
|
||||
from mcp_types.version import KNOWN_PROTOCOL_VERSIONS
|
||||
|
||||
from tests.interaction._requirements import (
|
||||
CONNECTABLE_TRANSPORTS,
|
||||
REQUIREMENTS,
|
||||
SPEC_2026_BASE_URL,
|
||||
SPEC_BASE_URL,
|
||||
SPEC_VERSIONS,
|
||||
ArmExclusion,
|
||||
KnownFailure,
|
||||
Requirement,
|
||||
SpecVersion,
|
||||
Transport,
|
||||
cell_id,
|
||||
compute_cells,
|
||||
covered_by,
|
||||
requirement,
|
||||
)
|
||||
from tests.interaction.conftest import _FACTORIES
|
||||
|
||||
_SUITE_ROOT = Path(__file__).parent
|
||||
_REPO_ROOT = _SUITE_ROOT.parent.parent
|
||||
|
||||
# Repo paths cited inside deferral reasons ("Covered by tests/... ").
|
||||
_CITED_PATH = re.compile(r"(?:tests|src)/[\w./-]*\w")
|
||||
|
||||
# Tests that exercise the suite's own helpers rather than an interaction-model behaviour.
|
||||
# Anything listed here is exempt from the every-test-has-a-requirement check.
|
||||
_HARNESS_SELF_TESTS = {
|
||||
"tests.interaction.lowlevel.test_wire.test_recording_read_stream_ends_iteration_when_the_sender_closes",
|
||||
"tests.interaction.transports.test_bridge.test_response_chunks_arrive_as_the_application_sends_them",
|
||||
"tests.interaction.transports.test_bridge.test_closing_the_response_delivers_a_disconnect_to_the_application",
|
||||
"tests.interaction.transports.test_bridge.test_an_application_failure_before_the_response_starts_fails_the_request",
|
||||
"tests.interaction.transports.test_bridge.test_disabling_cancel_on_close_lets_the_application_finish_after_disconnect",
|
||||
"tests.interaction.auth.test_flow.test_shimmed_app_serves_overrides_404s_and_otherwise_forwards_to_the_wrapped_app",
|
||||
}
|
||||
|
||||
|
||||
def _import_all_test_modules() -> list[ModuleType]:
|
||||
"""Import every other test module in the suite so their `@requirement` decorators register."""
|
||||
modules: list[ModuleType] = []
|
||||
for path in sorted(_SUITE_ROOT.rglob("test_*.py")):
|
||||
relative = path.relative_to(_SUITE_ROOT).with_suffix("")
|
||||
name = f"{__package__}.{'.'.join(relative.parts)}"
|
||||
if name != __name__:
|
||||
modules.append(importlib.import_module(name))
|
||||
return modules
|
||||
|
||||
|
||||
def test_every_requirement_is_exercised() -> None:
|
||||
"""Each non-deferred requirement is covered by at least one test (deferred ones by none)."""
|
||||
_import_all_test_modules()
|
||||
|
||||
uncovered = [
|
||||
requirement_id
|
||||
for requirement_id, spec in sorted(REQUIREMENTS.items())
|
||||
if spec.deferred is None and not covered_by(requirement_id)
|
||||
]
|
||||
assert not uncovered, f"Requirements with no test and no deferred reason: {uncovered}"
|
||||
|
||||
stale_deferrals = [
|
||||
requirement_id
|
||||
for requirement_id, spec in sorted(REQUIREMENTS.items())
|
||||
if spec.deferred is not None and covered_by(requirement_id)
|
||||
]
|
||||
assert not stale_deferrals, f"Deferred requirements that now have tests (remove deferred): {stale_deferrals}"
|
||||
|
||||
|
||||
def test_every_test_exercises_a_requirement() -> None:
|
||||
"""Each test in the suite carries at least one `@requirement` mark (harness self-tests excepted)."""
|
||||
all_tests = {
|
||||
f"{module.__name__}.{name}"
|
||||
for module in _import_all_test_modules()
|
||||
for name in vars(module)
|
||||
if name.startswith("test_")
|
||||
}
|
||||
linked_tests = {test_name for requirement_id in REQUIREMENTS for test_name in covered_by(requirement_id)}
|
||||
|
||||
unlinked = sorted(all_tests - linked_tests - _HARNESS_SELF_TESTS)
|
||||
assert not unlinked, f"Tests with no @requirement mark: {unlinked}"
|
||||
|
||||
stale_exemptions = sorted(_HARNESS_SELF_TESTS - all_tests)
|
||||
assert not stale_exemptions, f"Harness self-test exemptions that no longer exist: {stale_exemptions}"
|
||||
|
||||
|
||||
def test_deferral_reasons_cite_existing_paths() -> None:
|
||||
"""Every repo path named in a deferral reason exists, so coverage pointers cannot rot."""
|
||||
missing = sorted(
|
||||
f"{requirement_id}: {cited}"
|
||||
for requirement_id, spec in REQUIREMENTS.items()
|
||||
if spec.deferred is not None
|
||||
for cited in _CITED_PATH.findall(spec.deferred)
|
||||
if not (_REPO_ROOT / cited).exists()
|
||||
)
|
||||
assert not missing, f"Deferral reasons citing paths that do not exist: {missing}"
|
||||
|
||||
|
||||
def test_spec_versions_are_known_and_include_latest() -> None:
|
||||
"""Every active spec version is one the SDK knows about, and the SDK's latest is on the active axis."""
|
||||
assert set(SPEC_VERSIONS) <= set(KNOWN_PROTOCOL_VERSIONS)
|
||||
assert LATEST_PROTOCOL_VERSION in SPEC_VERSIONS
|
||||
|
||||
|
||||
def test_spec_base_urls_are_pinned_to_their_revision() -> None:
|
||||
"""SPEC_BASE_URL constants are pinned literals, so growing SPEC_VERSIONS cannot repoint existing source links."""
|
||||
assert SPEC_BASE_URL == "https://modelcontextprotocol.io/specification/2025-11-25"
|
||||
assert SPEC_2026_BASE_URL == "https://modelcontextprotocol.io/specification/2026-07-28"
|
||||
|
||||
|
||||
def test_connectable_transports_match_connect_factories() -> None:
|
||||
"""CONNECTABLE_TRANSPORTS and the conftest factory map name exactly the same transports."""
|
||||
assert set(CONNECTABLE_TRANSPORTS) == set(_FACTORIES)
|
||||
|
||||
|
||||
def test_supersession_links_are_symmetric_and_versioned() -> None:
|
||||
"""``supersedes``/``superseded_by`` reference real entries, agree in both directions, and carry version bounds."""
|
||||
broken = [
|
||||
f"{req_id} -> {target}"
|
||||
for req_id, req in REQUIREMENTS.items()
|
||||
for target in req.supersedes
|
||||
if target not in REQUIREMENTS or REQUIREMENTS[target].superseded_by != req_id or req.added_in is None
|
||||
] + [
|
||||
f"{req_id} <- {req.superseded_by}"
|
||||
for req_id, req in REQUIREMENTS.items()
|
||||
if req.superseded_by is not None
|
||||
if req.superseded_by not in REQUIREMENTS
|
||||
or req_id not in REQUIREMENTS[req.superseded_by].supersedes
|
||||
or req.removed_in is None
|
||||
]
|
||||
assert not broken, f"Broken supersession links (forward '->' or back '<-'): {broken}"
|
||||
|
||||
|
||||
def test_removed_entry_has_disposition() -> None:
|
||||
"""Every retired requirement carries either a forward link or a prose note explaining the retirement."""
|
||||
undisposed = [
|
||||
req_id
|
||||
for req_id, req in REQUIREMENTS.items()
|
||||
if req.removed_in is not None and req.superseded_by is None and req.note is None
|
||||
]
|
||||
assert not undisposed, f"Requirements with removed_in but no superseded_by or note: {undisposed}"
|
||||
|
||||
|
||||
def test_transport_restriction_has_note() -> None:
|
||||
"""Every transport-restricted requirement carries a note explaining why it is transport-specific."""
|
||||
missing = [req_id for req_id, req in REQUIREMENTS.items() if req.transports is not None and req.note is None]
|
||||
assert not missing, f"Requirements with transports= but no note: {missing}"
|
||||
|
||||
|
||||
def test_every_arm_exclusion_targets_a_reachable_cell() -> None:
|
||||
"""Every arm exclusion names a connectable transport (or wildcards).
|
||||
|
||||
spec_version is type-checked against the SpecVersion Literal and may reference a version not yet
|
||||
on the active SPEC_VERSIONS axis, so pre-staged exclusions for an upcoming revision are permitted.
|
||||
"""
|
||||
unreachable = [
|
||||
f"{req_id}: {exclusion}"
|
||||
for req_id, req in REQUIREMENTS.items()
|
||||
for exclusion in req.arm_exclusions
|
||||
if exclusion.transport is not None and exclusion.transport not in CONNECTABLE_TRANSPORTS
|
||||
]
|
||||
assert not unreachable, f"Arm exclusions targeting unreachable cells: {unreachable}"
|
||||
|
||||
|
||||
def test_every_known_failure_targets_a_reachable_cell() -> None:
|
||||
"""Every known failure names a connectable transport (or wildcards).
|
||||
|
||||
spec_version is type-checked against the SpecVersion Literal and may reference a version not yet
|
||||
on the active SPEC_VERSIONS axis, so pre-staged exclusions for an upcoming revision are permitted.
|
||||
"""
|
||||
unreachable = [
|
||||
f"{req_id}: {failure}"
|
||||
for req_id, req in REQUIREMENTS.items()
|
||||
for failure in req.known_failures
|
||||
if failure.transport is not None and failure.transport not in CONNECTABLE_TRANSPORTS
|
||||
]
|
||||
assert not unreachable, f"Known failures targeting unreachable cells: {unreachable}"
|
||||
|
||||
|
||||
def test_unknown_requirement_id_is_rejected() -> None:
|
||||
"""Marking a test with an ID that is not in the manifest fails at decoration time."""
|
||||
with pytest.raises(KeyError, match="Unknown requirement id 'tools:call:does-not-exist'"):
|
||||
requirement("tools:call:does-not-exist")
|
||||
|
||||
|
||||
def test_invalid_requirement_source_is_rejected() -> None:
|
||||
"""A requirement whose source is not a spec URL, 'sdk', or an issue reference fails at construction."""
|
||||
with pytest.raises(ValueError, match="source must be a specification URL"):
|
||||
Requirement(source="https://example.com/not-the-spec", behavior="Never constructed.")
|
||||
|
||||
|
||||
def test_arm_exclusion_with_unknown_spec_version_is_rejected() -> None:
|
||||
"""An arm exclusion naming a spec version outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
|
||||
with pytest.raises(ValueError, match="is not in KNOWN_PROTOCOL_VERSIONS"):
|
||||
ArmExclusion(reason="requires-session", spec_version=cast("SpecVersion", "2099-01-01"))
|
||||
|
||||
|
||||
def test_known_failure_with_empty_note_is_rejected() -> None:
|
||||
"""A known failure with a blank note fails at construction."""
|
||||
with pytest.raises(ValueError, match="note must be non-empty"):
|
||||
KnownFailure(note=" ")
|
||||
|
||||
|
||||
def test_known_failure_with_unknown_spec_version_is_rejected() -> None:
|
||||
"""A known failure naming a spec version outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
|
||||
with pytest.raises(ValueError, match="is not in KNOWN_PROTOCOL_VERSIONS"):
|
||||
KnownFailure(note="x", spec_version=cast("SpecVersion", "2099-01-01"))
|
||||
|
||||
|
||||
def test_known_failure_with_malformed_issue_is_rejected() -> None:
|
||||
"""A known failure whose issue reference is neither '#<n>' nor a GitHub URL fails at construction."""
|
||||
with pytest.raises(ValueError, match="must be '#<n>' or a GitHub URL"):
|
||||
KnownFailure(note="x", issue="not-a-link")
|
||||
|
||||
|
||||
def test_requirement_with_unknown_added_in_is_rejected() -> None:
|
||||
"""A requirement whose added_in is outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
|
||||
with pytest.raises(ValueError, match="added_in .* is not in KNOWN_PROTOCOL_VERSIONS"):
|
||||
Requirement(source="sdk", behavior="x", added_in=cast("SpecVersion", "2099-01-01"))
|
||||
|
||||
|
||||
def test_requirement_with_unknown_removed_in_is_rejected() -> None:
|
||||
"""A requirement whose removed_in is outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
|
||||
with pytest.raises(ValueError, match="removed_in .* is not in KNOWN_PROTOCOL_VERSIONS"):
|
||||
Requirement(source="sdk", behavior="x", removed_in=cast("SpecVersion", "2099-01-01"))
|
||||
|
||||
|
||||
def test_requirement_with_empty_version_range_is_rejected() -> None:
|
||||
"""A requirement whose added_in is not strictly earlier than its removed_in fails at construction."""
|
||||
with pytest.raises(ValueError, match="must be earlier than"):
|
||||
Requirement(source="sdk", behavior="x", added_in="2025-11-25", removed_in="2025-11-25")
|
||||
|
||||
|
||||
def _req(
|
||||
*,
|
||||
added_in: SpecVersion | None = None,
|
||||
removed_in: SpecVersion | None = None,
|
||||
transports: tuple[Transport, ...] | None = None,
|
||||
arm_exclusions: tuple[ArmExclusion, ...] = (),
|
||||
known_failures: tuple[KnownFailure, ...] = (),
|
||||
) -> Requirement:
|
||||
"""Build a synthetic Requirement for compute_cells() unit tests."""
|
||||
return Requirement(
|
||||
source="sdk",
|
||||
behavior="x",
|
||||
added_in=added_in,
|
||||
removed_in=removed_in,
|
||||
transports=transports,
|
||||
arm_exclusions=arm_exclusions,
|
||||
known_failures=known_failures,
|
||||
)
|
||||
|
||||
|
||||
def test_compute_cells_with_no_requirements_yields_full_grid() -> None:
|
||||
"""With a single-version axis, an empty requirement list yields one cell per connectable transport."""
|
||||
cells = compute_cells([], spec_versions=("2025-11-25",))
|
||||
assert [c.id for c in cells] == ["in-memory", "sse", "streamable-http", "streamable-http-stateless"]
|
||||
assert [c.values for c in cells] == [
|
||||
(("in-memory", "2025-11-25"),),
|
||||
(("sse", "2025-11-25"),),
|
||||
(("streamable-http", "2025-11-25"),),
|
||||
(("streamable-http-stateless", "2025-11-25"),),
|
||||
]
|
||||
|
||||
|
||||
def test_compute_cells_intersects_stacked_version_ranges() -> None:
|
||||
"""Stacked requirements intersect their [added_in, removed_in) windows: a cell survives only if all admit it."""
|
||||
cells = compute_cells(
|
||||
[_req(removed_in="2026-07-28"), _req(added_in="2025-11-25")],
|
||||
spec_versions=("2025-11-25", "2026-07-28"),
|
||||
)
|
||||
assert [c.id for c in cells] == [
|
||||
"in-memory-2025-11-25",
|
||||
"sse-2025-11-25",
|
||||
"streamable-http-2025-11-25",
|
||||
"streamable-http-stateless-2025-11-25",
|
||||
]
|
||||
|
||||
|
||||
def test_compute_cells_drops_era_locked_transport_outside_its_versions() -> None:
|
||||
"""A transport listed in TRANSPORT_SPEC_VERSIONS only appears for the spec versions it serves."""
|
||||
cells = compute_cells([], spec_versions=("2025-11-25", "2026-07-28"))
|
||||
assert [c.id for c in cells] == [
|
||||
"in-memory-2025-11-25",
|
||||
"sse-2025-11-25",
|
||||
"streamable-http-2025-11-25",
|
||||
"streamable-http-stateless-2025-11-25",
|
||||
"in-memory-2026-07-28",
|
||||
"streamable-http-2026-07-28",
|
||||
]
|
||||
|
||||
|
||||
def test_compute_cells_honours_arm_exclusion_from_any_stacked_requirement() -> None:
|
||||
"""An arm exclusion on any stacked requirement drops the matching cell even when other requirements have none."""
|
||||
cells = compute_cells(
|
||||
[_req(), _req(arm_exclusions=(ArmExclusion(reason="requires-session", transport="sse"),))],
|
||||
spec_versions=("2025-11-25",),
|
||||
)
|
||||
assert [c.id for c in cells] == ["in-memory", "streamable-http", "streamable-http-stateless"]
|
||||
|
||||
|
||||
def test_compute_cells_wildcard_arm_exclusion_drops_every_cell() -> None:
|
||||
"""An arm exclusion with both transport and spec_version unset matches every cell, leaving none."""
|
||||
cells = compute_cells([_req(arm_exclusions=(ArmExclusion(reason="requires-session"),))])
|
||||
assert cells == []
|
||||
|
||||
|
||||
def test_compute_cells_marks_known_failure_as_strict_xfail() -> None:
|
||||
"""A known failure attaches a strict xfail mark to exactly the matching cell and leaves others unmarked."""
|
||||
cells = compute_cells(
|
||||
[_req(known_failures=(KnownFailure(note="broken on sse", transport="sse"),))],
|
||||
spec_versions=("2025-11-25",),
|
||||
)
|
||||
by_id = {c.id: c for c in cells}
|
||||
assert set(by_id) == {"in-memory", "sse", "streamable-http", "streamable-http-stateless"}
|
||||
assert by_id["sse"].marks[0].name == "xfail"
|
||||
assert by_id["sse"].marks[0].kwargs == {"reason": "broken on sse", "strict": True}
|
||||
assert by_id["in-memory"].marks == ()
|
||||
assert by_id["streamable-http"].marks == ()
|
||||
assert by_id["streamable-http-stateless"].marks == ()
|
||||
|
||||
|
||||
def test_compute_cells_wildcard_known_failure_marks_every_cell() -> None:
|
||||
"""A known failure with both transport and spec_version unset marks every emitted cell as strict xfail."""
|
||||
cells = compute_cells([_req(known_failures=(KnownFailure(note="all broken"),))], spec_versions=("2025-11-25",))
|
||||
assert len(cells) == 4
|
||||
assert all(c.marks[0].name == "xfail" for c in cells)
|
||||
assert all(c.marks[0].kwargs == {"reason": "all broken", "strict": True} for c in cells)
|
||||
|
||||
|
||||
def test_compute_cells_ignores_transports_field() -> None:
|
||||
"""Requirement.transports is descriptive metadata only and does not filter the cell grid."""
|
||||
cells = compute_cells([_req(transports=("stdio",))], spec_versions=("2025-11-25",))
|
||||
assert [c.id for c in cells] == list(CONNECTABLE_TRANSPORTS)
|
||||
|
||||
|
||||
def test_cell_id_omits_version_when_single_spec_version() -> None:
|
||||
"""With a single-version axis the cell id is just the transport name."""
|
||||
assert cell_id("sse", "2025-11-25", spec_versions=("2025-11-25",)) == "sse"
|
||||
|
||||
|
||||
def test_cell_id_appends_version_when_multiple_spec_versions() -> None:
|
||||
"""With more than one active spec version the cell id gains a -<version> suffix."""
|
||||
assert cell_id("sse", "2025-11-25", spec_versions=("2025-11-25", "2026-07-28")) == "sse-2025-11-25"
|
||||
Reference in New Issue
Block a user