Skip to content

Commit d07f01f

Browse files
committed
test: track the full requirements surface in the interaction manifest
1 parent bdfded0 commit d07f01f

7 files changed

Lines changed: 2082 additions & 539 deletions

File tree

tests/interaction/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,15 @@ test body — each directory pins its flavour's true output exactly.
6060
- **`source`** is a deep link into the MCP specification for externally mandated behaviour,
6161
the literal string `"sdk"` for behaviour the SDK chose where the spec is silent, or
6262
`"issue:#n"` for a regression lock.
63-
- **`behavior`** describes what the suite *asserts* — which is always the SDK's current
64-
behaviour, never an aspiration.
65-
- **`divergence`** records the gap when current behaviour differs from what `source` mandates,
66-
with an issue link once one exists. The test still pins current behaviour.
67-
- **`deferred`** marks a behaviour that is deliberately not covered, with the reason.
63+
- **`behavior`** describes the *required* behaviour — what the specification (or the SDK's own
64+
contract) says should happen. Tests always pin the SDK's current behaviour; where that falls
65+
short of `behavior`, the gap is recorded as data rather than hidden in the test.
66+
- **`divergence`** records that gap for entries whose tests pin the divergent current behaviour.
67+
- **`deferred`** marks a behaviour that is tracked but not yet covered by a test in this suite.
68+
The reason names the covering tests elsewhere in the repo, starts with "Not implemented in the
69+
SDK" for genuine feature gaps, or starts with "Not yet covered here" for tests that are planned.
70+
- **`transports`** names the transports a behaviour applies to; omitted means transport-independent.
71+
- **`issue`** carries the tracking link for a recorded gap once one is filed.
6872

6973
Tests link themselves to the manifest with a decorator:
7074

tests/interaction/_requirements.py

Lines changed: 2049 additions & 533 deletions
Large diffs are not rendered by default.

tests/interaction/lowlevel/test_cancellation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121

2222
@requirement("protocol:cancel:in-flight")
23+
@requirement("protocol:cancel:handler-abort-propagates")
2324
async def test_cancellation_stops_in_flight_handler() -> None:
2425
"""Cancelling an in-flight request interrupts its handler and fails the pending call.
2526

tests/interaction/lowlevel/test_completion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121

2222
@requirement("completion:prompt-arg")
23+
@requirement("completion:result-shape")
2324
async def test_complete_prompt_argument() -> None:
2425
"""Completing a prompt argument delivers the ref, argument name, and current value to the handler.
2526

tests/interaction/lowlevel/test_initialize.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
pytestmark = pytest.mark.anyio
4747

4848

49+
@requirement("lifecycle:initialize:basic")
4950
@requirement("lifecycle:initialize:server-info")
5051
async def test_initialize_returns_server_info() -> None:
5152
"""Every identity field the server declares is returned to the client in server_info."""

tests/interaction/lowlevel/test_ping.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
pytestmark = pytest.mark.anyio
1313

1414

15+
@requirement("lifecycle:ping")
1516
@requirement("ping:client-to-server")
1617
async def test_client_ping_returns_empty_result() -> None:
1718
"""A client ping is answered with an empty result, even by a server with no handlers."""
@@ -23,6 +24,7 @@ async def test_client_ping_returns_empty_result() -> None:
2324
assert result == snapshot(EmptyResult())
2425

2526

27+
@requirement("lifecycle:ping")
2628
@requirement("ping:server-to-client")
2729
async def test_server_ping_returns_empty_result() -> None:
2830
"""A server-initiated ping sent while a request is in flight is answered by the client.

tests/interaction/test_coverage.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
33
The contract runs in both directions: every non-deferred entry in :data:`REQUIREMENTS` must be
44
exercised by at least one test, and every test in the suite must carry at least one
5-
`@requirement(...)` mark referencing a manifest entry. Test modules are imported directly
5+
`@requirement(...)` mark referencing a manifest entry. Deferral reasons that point at coverage
6+
elsewhere in the repo must point at paths that exist. Test modules are imported directly
67
(rather than relying on pytest collection) so the check holds even when only this file is run.
78
"""
89

910
import importlib
11+
import re
1012
from pathlib import Path
1113
from types import ModuleType
1214

@@ -15,6 +17,10 @@
1517
from tests.interaction._requirements import REQUIREMENTS, Requirement, covered_by, requirement
1618

1719
_SUITE_ROOT = Path(__file__).parent
20+
_REPO_ROOT = _SUITE_ROOT.parent.parent
21+
22+
# Repo paths cited inside deferral reasons ("Covered by tests/... ").
23+
_CITED_PATH = re.compile(r"(?:tests|src)/[\w./-]*\w")
1824

1925
# Tests that exercise the suite's own helpers rather than an interaction-model behaviour.
2026
# Anything listed here is exempt from the every-test-has-a-requirement check.
@@ -70,6 +76,18 @@ def test_every_test_exercises_a_requirement() -> None:
7076
assert not stale_exemptions, f"Harness self-test exemptions that no longer exist: {stale_exemptions}"
7177

7278

79+
def test_deferral_reasons_cite_existing_paths() -> None:
80+
"""Every repo path named in a deferral reason exists, so coverage pointers cannot rot."""
81+
missing = sorted(
82+
f"{requirement_id}: {cited}"
83+
for requirement_id, spec in REQUIREMENTS.items()
84+
if spec.deferred is not None
85+
for cited in _CITED_PATH.findall(spec.deferred)
86+
if not (_REPO_ROOT / cited).exists()
87+
)
88+
assert not missing, f"Deferral reasons citing paths that do not exist: {missing}"
89+
90+
7391
def test_unknown_requirement_id_is_rejected() -> None:
7492
"""Marking a test with an ID that is not in the manifest fails at decoration time."""
7593
with pytest.raises(KeyError, match="Unknown requirement id 'tools:call:does-not-exist'"):

0 commit comments

Comments
 (0)