Skip to content

Commit 9338291

Browse files
committed
tests: close the docs test modules' branch-coverage gaps
CI requires 100% branch coverage over tests/ as well as src/, and the new docs test modules left four statements and a handful of branch arcs uncovered. The statements were real gaps, each closed by making the test stronger rather than weaker: - test_deploy: the shared-key-different-name test now also lands the retry back on the instance that minted the token and asserts it completes. That is the half of the story the page tells, and it is exactly what the sibling default-key test already proved. - test_troubleshooting: two decorated functions whose bodies can never run (one's decoration is what raises; the other is the duplicate that gets dropped) now have docstring-only bodies, and the connection-fails case enters the client explicitly with __aenter__() (the shape tests/client/test_client.py already uses) instead of an `async with` whose body is unreachable by design. The rest were not real: every remaining flagged line executes (zero missed statements); coverage.py misattributes arcs around nested `async with` bodies on newer Pythons, worst on 3.14, which is exactly the case AGENTS.md documents for `# pragma: no branch` (branch arcs only; ~180 existing uses across src/ and tests/). Six of those, one of them on a straight-line test that raises nothing at all. ./scripts/test (the CI-equivalent gate) now reports 100.00% and strict-no-cover passes.
1 parent 7d3ea3b commit 9338291

3 files changed

Lines changed: 18 additions & 14 deletions

File tree

tests/docs_src/test_deploy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,12 @@ async def refund(amount: int, ctx: Context) -> str | InputRequiredResult:
160160
token = await _first_round(on_one, 120)
161161
with pytest.raises(MCPError) as exc:
162162
await _retry(on_two, 120, token)
163+
# Same keys AND the same name: back on the instance that minted it, the retry completes.
164+
second = await _retry(on_one, 120, token)
163165

164166
_assert_frozen_rejection(exc)
167+
assert isinstance(second, CallToolResult)
168+
assert second.content == [TextContent(type="text", text="refunded $120")]
165169

166170

167171
# -- change notifications across replicas ----------------------------------------------

tests/docs_src/test_legacy_clients.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ async def test_stateless_http_kills_the_legacy_back_channel_and_only_the_legacy_
105105
legacy_target = streamable_http_client(URL, http_client=http)
106106
async with Client(legacy_target, mode="legacy", elicitation_callback=tutorial001.answer) as legacy:
107107
assert legacy.protocol_version == "2025-11-25"
108-
with pytest.raises(MCPError) as exc_info:
108+
with pytest.raises(MCPError) as exc_info: # pragma: no branch
109109
await legacy.call_tool("reserve", {"title": "Dune"})
110110
assert exc_info.value.error.code == INVALID_REQUEST
111111
assert exc_info.value.error.message == (

tests/docs_src/test_troubleshooting.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ async def test_the_tool_decorator_without_parentheses_raises_at_import_time() ->
9898
with pytest.raises(TypeError, match=r"Use @tool\(\) instead of @tool"):
9999

100100
@undecorated
101-
def forecast(city: str) -> str:
102-
"""Today's forecast for one city."""
103-
return f"{city}: Rain."
101+
def forecast(city: str) -> None:
102+
"""Today's forecast for one city. Never called: the decoration itself is what raises."""
104103

105104

106105
async def test_a_duplicate_tool_name_keeps_the_first_and_drops_the_second() -> None:
@@ -116,9 +115,8 @@ async def test_a_duplicate_registration_logs_tool_already_exists(caplog: pytest.
116115
with caplog.at_level(logging.WARNING, logger="mcp.server.mcpserver.tools.tool_manager"):
117116

118117
@tutorial002.mcp.tool(name="forecast")
119-
def forecast_weekly(city: str) -> str:
120-
"""The week ahead for one city."""
121-
return f"{city}: Rain all week."
118+
def forecast_weekly(city: str) -> None:
119+
"""The week ahead for one city. Never called: it is the duplicate that gets dropped."""
122120

123121
assert "Tool already exists: forecast" in caplog.messages
124122

@@ -140,9 +138,9 @@ async def test_the_default_streamable_http_app_answers_a_real_hostname_with_421(
140138
assert "Invalid Host header: mcp.example.com" in caplog.messages
141139
# What the python `Client` raises instead: the generic stand-in, wrapped by the task group.
142140
async with httpx.AsyncClient(transport=transport) as http_client:
143-
with pytest.raises(Exception) as exc_info:
144-
async with Client(streamable_http_client("http://mcp.example.com/mcp", http_client=http_client)):
145-
pass # never reached: the handshake itself is what fails
141+
client = Client(streamable_http_client("http://mcp.example.com/mcp", http_client=http_client))
142+
with pytest.raises(Exception) as exc_info: # pragma: no branch
143+
await client.__aenter__() # the connection attempt itself is what fails
146144
assert not isinstance(exc_info.value, MCPError)
147145
assert exc_info.group_contains(MCPError, match="^Server returned an error response$")
148146

@@ -152,7 +150,8 @@ async def test_an_allowlisted_hostname_connects_and_calls_a_tool() -> None:
152150
transport = httpx.ASGITransport(app=tutorial004.app)
153151
async with tutorial004.mcp.session_manager.run():
154152
async with httpx.AsyncClient(transport=transport) as http_client:
155-
async with Client(streamable_http_client("http://mcp.example.com/mcp", http_client=http_client)) as c:
153+
allowed = streamable_http_client("http://mcp.example.com/mcp", http_client=http_client)
154+
async with Client(allowed) as c: # pragma: no branch
156155
assert c.protocol_version == "2026-07-28"
157156
result = await c.call_tool("forecast", {"city": "London"})
158157
assert result.structured_content == {"result": "London: Rain."}
@@ -247,8 +246,9 @@ async def test_ctx_elicit_over_stateless_http_has_no_back_channel() -> None:
247246
transport = httpx.ASGITransport(app=tutorial008.app)
248247
async with tutorial008.mcp.session_manager.run():
249248
async with httpx.AsyncClient(transport=transport) as http_client:
250-
async with Client(streamable_http_client("http://127.0.0.1:8000/mcp", http_client=http_client)) as c:
251-
with pytest.raises(MCPError) as exc_info:
249+
stateless = streamable_http_client("http://127.0.0.1:8000/mcp", http_client=http_client)
250+
async with Client(stateless) as c: # pragma: no branch
251+
with pytest.raises(MCPError) as exc_info: # pragma: no branch
252252
await c.call_tool("book_table", {"date": "Friday"})
253253
assert exc_info.value.error == ErrorData(
254254
code=INVALID_REQUEST,
@@ -263,7 +263,7 @@ async def test_a_request_state_the_server_did_not_mint_is_rejected(caplog: pytes
263263
"""The wire message is deliberately frozen; the real reason goes only to the server log."""
264264
async with Client(tutorial001.mcp) as client:
265265
with caplog.at_level(logging.WARNING, logger="mcp.server.request_state"):
266-
with pytest.raises(MCPError) as exc_info:
266+
with pytest.raises(MCPError) as exc_info: # pragma: no branch
267267
await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a")
268268
assert exc_info.value.error == ErrorData(
269269
code=INVALID_PARAMS, message="Invalid or expired requestState", data={"reason": "invalid_request_state"}

0 commit comments

Comments
 (0)