test(mcp): compare the installed SDK against the pyproject pin for every 2.0-only file
Move the require_mcp_2_sdk fixture from tests/tools/conftest.py to the root tests/conftest.py so tests/test_mcp_serve.py can use it too, and read the required version from the `mcp==X` pin in pyproject's [mcp] extra instead of a hardcoded "2.0.0" — the pin moves, the guard follows. Sharpen the remaining presence-only guards named in the issue that still went red under an installed-but-older SDK on current main: tests/test_mcp_serve.py (the mcp_server_e2e fixture and TestServerCreation asserted `mcp.server.MCPServer`, absent in 1.x) and tests/tools/test_mcp_oauth.py::TestCallbackPortReservation (2.0 AuthorizationCodeResult vs the 1.x tuple). test_mcp_oauth_manager.py and test_mcp_device_flow.py already pass under 1.28.1 on current main, so they are left alone; PR #114068's importorskip hunks in those files do not overlap these edits.
This commit is contained in:
@@ -1266,6 +1266,40 @@ def _relocate_basetemp_outside_operator_home(config) -> None:
|
||||
config.option.basetemp = str(safe)
|
||||
|
||||
|
||||
def _pinned_mcp_sdk_version() -> str:
|
||||
"""The ``mcp==X`` pin carried by the ``[mcp]`` extra in pyproject.toml."""
|
||||
import tomllib
|
||||
|
||||
with open(Path(__file__).resolve().parent.parent / "pyproject.toml", "rb") as fh:
|
||||
extras = tomllib.load(fh)["project"]["optional-dependencies"]
|
||||
for req in extras["mcp"]:
|
||||
if req.startswith("mcp=="):
|
||||
return req.split("==", 1)[1].strip()
|
||||
raise RuntimeError("pyproject.toml [mcp] extra no longer pins mcp==X")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def require_mcp_2_sdk():
|
||||
"""Skip tests that pin mcp 2.0-only behaviour when an older SDK is installed.
|
||||
|
||||
The runtime deliberately supports both SDK generations (the dual streamable-client probe in
|
||||
mcp_tool), so a stale ``mcp`` distribution imports fine and presence-only guards let these
|
||||
tests through — where they fail later with opaque SDK errors. Compare the installed
|
||||
distribution against the pin so the outcome is an explicit skip with an actionable reason.
|
||||
"""
|
||||
from importlib.metadata import PackageNotFoundError, version as dist_version
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
pinned = _pinned_mcp_sdk_version()
|
||||
try:
|
||||
found = dist_version("mcp")
|
||||
except PackageNotFoundError:
|
||||
pytest.skip(f"requires mcp=={pinned} (not installed); install the [mcp] extra")
|
||||
if Version(found) < Version(pinned):
|
||||
pytest.skip(f"requires mcp=={pinned} (found {found}); install the [mcp] extra")
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True) # after _pytest.tmpdir has built config._tmp_path_factory
|
||||
def pytest_configure(config): # noqa: D401 — pytest hook
|
||||
"""Register markers used by hermetic conftest."""
|
||||
|
||||
@@ -522,9 +522,8 @@ class TestEventBridge:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def mcp_server_e2e(populated_sessions_dir, mock_session_db, monkeypatch):
|
||||
"""Create a fully wired MCP server for E2E testing."""
|
||||
mcp = pytest.importorskip("mcp", reason="MCP SDK not installed")
|
||||
def mcp_server_e2e(populated_sessions_dir, mock_session_db, monkeypatch, require_mcp_2_sdk):
|
||||
"""Create a fully wired MCP server for E2E testing (pinned SDK: 1.x lacks mcp.server.MCPServer)."""
|
||||
import mcp_serve
|
||||
monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
|
||||
monkeypatch.setattr(mcp_serve, "_get_session_db", lambda: mock_session_db)
|
||||
@@ -987,14 +986,14 @@ class TestToolRegistration:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestServerCreation:
|
||||
@pytest.mark.usefixtures("require_mcp_2_sdk")
|
||||
def test_create_server(self, populated_sessions_dir, monkeypatch):
|
||||
pytest.importorskip("mcp", reason="MCP SDK not installed")
|
||||
import mcp_serve
|
||||
monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
|
||||
assert mcp_serve.create_mcp_server() is not None
|
||||
|
||||
@pytest.mark.usefixtures("require_mcp_2_sdk")
|
||||
def test_create_with_bridge(self, populated_sessions_dir, monkeypatch):
|
||||
pytest.importorskip("mcp", reason="MCP SDK not installed")
|
||||
import mcp_serve
|
||||
monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
|
||||
bridge = mcp_serve.EventBridge()
|
||||
|
||||
@@ -55,29 +55,6 @@ def _materialize_mcp_sdk_symbols():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def require_mcp_2_sdk():
|
||||
"""Skip tests that pin mcp 2.0-only behaviour when an older SDK is installed.
|
||||
|
||||
The runtime deliberately supports both SDK generations (the dual
|
||||
streamable-client probe in mcp_tool), so a stale ``mcp`` distribution
|
||||
imports fine and presence-only guards let these tests through — where
|
||||
they fail later with opaque SDK errors. Compare the installed
|
||||
distribution against the 2.0.0 pin carried by the ``[mcp]`` extra so the
|
||||
outcome is an explicit skip with an actionable reason.
|
||||
"""
|
||||
from importlib.metadata import PackageNotFoundError, version as dist_version
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
try:
|
||||
found = dist_version("mcp")
|
||||
except PackageNotFoundError:
|
||||
pytest.skip("mcp SDK not installed; install the [mcp] extra")
|
||||
if Version(found) < Version("2.0.0"):
|
||||
pytest.skip(f"requires mcp>=2.0.0 (found {found}); install the [mcp] extra")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_web_result_cache():
|
||||
"""Reset the web_search TTL memo between tests.
|
||||
|
||||
@@ -543,6 +543,7 @@ class TestCallbackHandlerErrorEscaping:
|
||||
# TOCTOU port reservation (#22161)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.usefixtures("require_mcp_2_sdk")
|
||||
class TestCallbackPortReservation:
|
||||
"""The socket picked at selection time stays bound until callback bind.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user