Run SSE and Unicode transport tests in process instead of over sockets#2765
Conversation
Same class of fix as the security-test migration: these files spawned
uvicorn subprocesses on bind-then-close ports and polled for readiness,
which races under pytest-xdist when two workers pick the same port.
tests/shared/test_sse.py now drives the same Starlette apps in process:
sse_client connects through an httpx_client_factory backed by the
in-process streaming bridge, raw-httpx assertions use the same bridge,
and the mounted-app and request-context servers are built as plain app
factories instead of subprocess targets. Assertions are unchanged, with
two deliberate exceptions now that the server handlers run traced
in-process:
- test_sse_client_timeout is deleted. It has been permanently skipped
("highlights a possible bug in SSE read timeout exception handling"),
and its premise — a real network read timeout — cannot exist for an
in-process app, so it could never be unskipped here. The slow://
resource branch and sse_read_timeout plumbing existed only for it.
- Handlers that no test ever invoked (the main server's tools handlers,
the context server's unknown-tool fallthrough) are removed or
replaced with dispatch asserts, since unreachable branches now fail
branch coverage instead of hiding in an untraced subprocess.
tests/client/test_http_unicode.py gets the same treatment: the Unicode
echo server now runs in process and streamable_http_client speaks to it
through the bridge with follow_redirects enabled, matching the SDK's
own client factory (Starlette's Mount 307-redirects the bare /mcp
path).
There was a problem hiding this comment.
LGTM — test-only migration that follows the same in-process harness pattern already merged in #2764.
Extended reasoning...
Overview
This PR touches only two test files (tests/shared/test_sse.py and tests/client/test_http_unicode.py), replacing the subprocess + uvicorn + ephemeral-port harness with in-process Starlette apps driven through the existing StreamingASGITransport bridge from tests/interaction/transports. No production code under src/ is modified. The assertions of the surviving tests are preserved; the changes beyond the harness swap are deletion of a permanently-skipped test (test_sse_client_timeout), removal of dead handler branches that were never exercised, and docstring/annotation cleanup.
Security risks
None. The change is test-only. Disabling DNS-rebinding protection in the in-process test apps is appropriate — that protection guards against a network-level attack that cannot occur in-process, and its behaviour remains pinned by tests/server/test_sse_security.py, which I confirmed exists in the repository.
Level of scrutiny
Low-to-moderate. Test refactors carry the risk of silently weakening coverage rather than introducing bugs, and the author addresses the two coverage-relevant deletions explicitly: the deleted timeout test was @pytest.mark.skip since introduction and never executed, and the removed handler branches were unreachable. The bridge pattern (StreamingASGITransport, cancel_on_close=False for SSE drains, httpx_client_factory injection) is the same one already established and merged in #2764 and used by tests/interaction/transports/test_sse.py, so this is following an established repository pattern rather than introducing a new design.
Other factors
The bug hunting system found no bugs. The PR description reports a green full test run with 100% line+branch coverage and extensive stress testing under pytest-xdist. The change is self-contained, mechanical in nature, and consistent with the project's stated direction of migrating remaining socket-based test files in-process.
Second installment of the in-process test migration that started in #2764:
tests/shared/test_sse.pyandtests/client/test_http_unicode.pystill used the bind-then-close port pick + uvicorn subprocess + readiness-poll harness, which races under pytest-xdist when two workers grab the same ephemeral port.test_sse_session_cleanup_on_disconnecthas flaked exactly this way under parallel load.Motivation and Context
Same mechanism and fix as #2764. The two files now drive the same Starlette apps in process through
StreamingASGITransport(via the sanctionedtests.interaction.transportsre-export):tests/shared/test_sse.py:sse_clientconnects through anhttpx_client_factorybacked by the bridge — the same pattern the interaction suite's SSE leg uses — and the basic/mounted/request-context servers become plain app factories sharing onemake_app(server)builder. DNS-rebinding protection is disabled in the in-process apps (it guards against a network attack that can't exist here; the protection behaviour itself is pinned bytests/server/test_sse_security.py).tests/client/test_http_unicode.py: the Unicode echo server runs in process andstreamable_http_clientspeaks to it through the bridge withfollow_redirects=True, matching the SDK's own client factory (Starlette'sMount307-redirects the bare/mcppath; the subprocess version silently relied on redirect-following too).Assertions are unchanged, with two deliberate exceptions now that the server handlers run as traced in-process code:
test_sse_client_timeoutis deleted. It was@pytest.mark.skipfrom the day it was introduced ("highlights a possible bug in SSE read timeout exception handling") and carriedpragma: no cover, so no executing coverage is lost. Its premise — a real socket read timeout firing mid-stream — cannot occur for an in-process app, so it could never be unskipped here. Theslow://resource branch and thesse_read_timeout=0.5fixture plumbing existed only for it. Note for the record: the suspicion it encoded (an expiredsse_read_timeoutmay not surface an error to the in-flight request — theReadTimeoutpath insrc/mcp/client/sse.pyis stillpragma: lax no cover) is not tracked anywhere; nearest existing issue is client'sread_stream_writeropen after SSE disconnection hanging.receive()#1811, which is the same failure family with a different trigger. Happy to file a dedicated issue so deleting the test doesn't delete the suspicion.if ctx.request:arms were dead. They're removed or replaced with dispatch asserts (assert params.name in (...)), since unreachable branches now fail branch coverage instead of hiding in an untraced subprocess.The migrated
test_sse.pyalso gets the same prose pass the security files got in #2764: behaviour-sentence docstrings, full annotations, and the line-counter loop in the raw-connection test replaced by twoanext()assertions, which retires the file's lastpragma: no branch.tests/shared/test_streamable_http.pyis the remaining subprocess-based file and will be its own (final) migration, along with pruning the then-unusedwait_for_serverhelper.How Has This Been Tested?
./scripts/testgreen: 1526 passed, 100% line+branch coverage,strict-no-coverclean (the suite's skip count drops from 2 to 1 with the dead test removed)-n 2/4/8/16, sustained CPU load (loadavg >100), GC-canary runs forResourceWarningleaks underfilterwarnings=error, and failure-injection runs to verify clean teardown on assertion failure — zero failuresBreaking Changes
None — test-only.
Types of changes
Checklist
Additional context
Several migrated tests now overlap with
tests/interaction/transports/test_sse.pycoverage (basic connection, session-created callback, disconnect cleanup, happy/error round trips). Deduplicating is deliberately out of scope here — this PR keeps the harness swap auditable — but it's inventoried for a follow-up.AI Disclaimer