Skip to content

[v1.x] tests: backport interaction-model suite from main (527 tests, src/ untouched)#2717

Draft
maxisbey wants to merge 20 commits into
v1.xfrom
maxisbey/v1x-interaction-backport
Draft

[v1.x] tests: backport interaction-model suite from main (527 tests, src/ untouched)#2717
maxisbey wants to merge 20 commits into
v1.xfrom
maxisbey/v1x-interaction-backport

Conversation

@maxisbey
Copy link
Copy Markdown
Contributor

Backports tests/interaction/ (the end-to-end interaction-model suite from #2691) to v1.x, rewritten against the v1 public API. The result is a parity baseline: once main adds backwards-compatibility shims, this suite can be copied over verbatim and the diff between "v1 suite on v1" and "v1 suite on v2-with-shims" is the shim gap.

Where to look

  • tests/interaction/README.md — conventions, manifest model, transport matrix (unchanged from main).
  • tests/interaction/_requirements.py — same 362-entry manifest; behavior text is SDK-neutral so it carries over. The divergence/deferred data is updated where v1 behaves differently.
  • tests/interaction/_connect.py — the connection harness, rewritten for v1: factories yield ClientSession directly (v1 has no high-level Client), and mounted_app/build_streamable_http_app hand-assemble the Starlette app from StreamableHTTPSessionManager + auth routes (v1's lowlevel Server has no streamable_http_app() builder).
  • tests/interaction/auth/_provider.py — unchanged from main; v1's OAuthAuthorizationServerProvider Protocol is identical.
  • src/untouched (git diff v1.x -- src/ is empty).

What's covered

527 tests (same coverage as the 529 on main minus the deferrals below). Every transport-agnostic test runs over {in-memory, streamable HTTP, SSE} via the connect fixture; transport-specific tests, hosting/resumability/session management, and the full OAuth flow are exercised the same as on main. The suite deliberately spreads server→client session-access across v1's several public idioms (server.request_context, the request_ctx contextvar, FastMCP's Context param, ctx.request_context, mcp.get_context()) so each path is exercised.

Deferred — not expressible via the v1 public API (3)

Requirement Why
resources:templates:pagination v1's @server.list_resource_templates() only accepts a nullary handler; the inbound cursor cannot be read
transport:streamable-http:stateless-restrictions v1 stateless mode has no guard on server-initiated requests; the only observable outcome is a hang
client-auth:authorize:offline-access-consent v1's OAuthClientProvider predates SEP-2207 (no offline_access/prompt handling)

How Has This Been Tested?

uv run --frozen pytest tests/interaction/ -n 0 — 527 passed in ~5 s. Coverage and strict-no-cover are intentionally not enforced for this suite on v1.x.

Breaking Changes

None — tests-only. pyproject.toml adds [tool.inline-snapshot] (matching main) and excludes tests/interaction/ from pyright strict mode.

Types of changes

  • Test/infrastructure addition

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added or updated documentation as needed (tests/interaction/README.md)

AI Disclaimer

maxisbey added 20 commits May 29, 2026 13:31
Excludes tests/interaction from pyright until the backport restores type
correctness phase by phase.
…ses, RecordingTransport→Recording stream-pair wrapper
…ker registration

- conftest.py: add pytest_configure registering the 'requirement' marker
  (round-1 adv-3 S1 fix; without this every file is a PytestUnknownMarkWarning
  collection error under filterwarnings=['error']).
- _connect.py imports: drop Client/MCPServer/jsonrpc_message_adapter (not in v1);
  add timedelta, ClientSession, FastMCP. Mount KEPT (adversarial-v2-gate S1: SSE
  build_sse_app needs it).
- _connect.py annotations: Connect Protocol + all factory signatures retyped to
  Server[Any]|FastMCP / timedelta / ClientSession; kwarg order matches v1
  ClientSession.__init__; add _lowlevel() helper.

Function bodies untouched per plan-v2 H2; the 7 dead Client/MCPServer/adapter
refs carry temporary noqa:F821 until H3/H4/H5 rewrite them.
…_sse, parse_sse_messages, initialize_body bodies
…ver_streamable_http, client_via_http; gate: 3 smoke legs pass

Also:
- pyproject: add [tool.inline-snapshot] default-flags=["disable"] (matches main; without
  it -n0 runs use inline-snapshot active mode whose pydantic comparison mishandles
  extra="allow" models)
- conftest: suppress PytestUnraisableExceptionWarning/ResourceWarning — v1 streamable-HTTP
  server transport leaks memory streams on teardown (e.g. _handle_get_request only closes
  sse_stream_reader on the exception path); fixes are src/-side on main, out of scope here
- test_ping.py: convert to v1 decorator pattern using session-access pattern B (request_ctx
  contextvar) per the file→pattern assignment

Gate: tests/interaction/lowlevel/test_ping.py 6/6 pass (both tests × 3 transport legs).
…d_app body

Mirrors FastMCP.streamable_http_app()'s auth gating exactly (verifier derivation,
middleware, AS routes, RequireAuthMiddleware wrap, PRM routes); mounted_app now
calls the public builder. _connect.py is feature-complete; gate still 6/6.
…AS+RS app, ClientSession yield); auth smoke passes
…er/transports/auth

- transports/test_bridge (4, no edits), test_stdio (2 + _stdio_server rewrite)
- lowlevel/test_completion (5), test_logging (3), test_tools b1/3 (5)
- mcpserver/test_completion (1)
- auth/test_flow (5)
- _requirements.py: tools:call:unknown-name + protocol:error:internal-error divergences updated for v1
- lowlevel/{cancellation,flows,meta,progress,prompts,tools} complete
- mcpserver/test_prompts complete
- transports/{sse,streamable_http} complete (1 deferred: stateless-mode server-initiated guard absent in v1)
- _requirements.py: 3 divergence updates (tools:call:unknown-name already in w1; client:output-schema:auto-list)
… / 3 deferred

Parallel lane (14 N-risk files) + sequential lane (8 Y-risk files), all batches ≤5 tests.
Deferred: roots:list-changed (no v1 lowlevel decorator), resources:templates:pagination
(nullary-only), client-auth:authorize:offline-access-consent (v1 lacks SEP-2207).
Several expected gaps did not materialize (InMemoryTransport, REQUEST_TIMEOUT, FastMCP ctor
kwargs, verify_tokens=False) — all solvable with helpers or snapshot regen.
Adds the one N-risk deferral (transport:streamable-http:stateless-restrictions) that
phase-4 wave 2 logged in cant-express-v1.md but did not apply to the manifest. The other
three v1-API-gap deferrals (roots:list-changed, resources:templates:pagination,
client-auth:authorize:offline-access-consent) were already applied by their Y-risk
sequential-lane batches.

test_deferral_reasons_cite_existing_paths was already green (no v2-only paths cited).

Full suite: 524 pass / 0 fail. 67 deferred entries (4 v1-API-gap).
…ding + 3.11 dead-zone lines

- pyproject: add 'pragma: lax no cover' to exclude_lines (matches main)
- _connect.py: Protocol stub body, verifier-gate no-branch, SSE return-Response no-cover
- _harness.py: resource_server_url gate no-branch
- test_initialize/test_wire: tg.cancel_scope.cancel() lax-no-cover (cpython#106749, 3.11 only)
- test_sse: nested-async-with no-branch (3.11 arc)

All harness scaffolding; no port bugs. Local ./scripts/test → 100.00%.
…st 8.4, sse-starlette 2.1)

lowest-direct only; runtime floors unchanged.

- pytest>=8.4.0: tests/interaction/auth uses pytest.RaisesGroup (8.4+); matches main.
- sse-starlette>=2.1.0 (dev group): 1.x keeps a module-global anyio.Event
  (AppStatus.should_exit_event) bound to the first test's event loop, which breaks
  every subsequent in-process SSE response under the streaming bridge. v1's own
  tests run uvicorn in a subprocess so don't observe this. Runtime floor stays
  >=1.6.1 (the SDK works with 1.6.1 under uvicorn).
…etween tests

sse-starlette <3.0 stores an anyio.Event on AppStatus the first time an
EventSourceResponse runs, bound to that test's event loop. Under the in-process
bridge (one process, per-test event loops) every subsequent SSE response — both
the [sse] leg and connect_with_oauth's streamable-HTTP responses — fails with
'bound to a different event loop'. v1's own transport tests run uvicorn in a
subprocess and so never share a process across event loops.

The previous commit's >=2.1.0 dev floor was insufficient (2.1.0 still has the
class attribute; 3.x switched to a ContextVar). An autouse fixture that resets
the attribute after each test handles all versions including the 1.6.1 runtime
floor, so the dev floor is reverted and lowest-direct CI again exercises the
runtime constraint.

Verified: 1598 tests / 100.00% coverage on both highest and lowest-direct.
…direct

The 3.11 zero-cost-exception tracer dead-zone (python/cpython#106749) drops line
events for sync statements between a cancel-on-exit __aexit__ and the next real
await. On anyio 4.5.0 (lowest-direct) the unwind path differs from anyio 4.10
just enough that three additional post-async-with assertions fall in the
dead-zone on 3.11 only. Same family as the markers added in 361bb9d.
connect_sse now removes _read_stream_writers[session_id] in a finally once the
GET request unwinds (#2719), so the endpoint-event test waits for that cleanup
after the client disconnects instead of pinning the old retention behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant