|
11 | 11 | from pathlib import Path |
12 | 12 | from typing import TYPE_CHECKING, Any |
13 | 13 |
|
| 14 | +# Pre-import pydantic.root_model and beartype.claw._clawstate to work |
| 15 | +# around a Pydantic + beartype + coverage interaction that breaks test |
| 16 | +# collection when both packages are involved (e.g. anything importing |
| 17 | +# mcp.types). Keep these imports above pytest. |
| 18 | +import pydantic.root_model |
| 19 | + |
| 20 | + |
| 21 | +with contextlib.suppress(ImportError): |
| 22 | + import beartype.claw._clawstate |
| 23 | + |
14 | 24 | import pytest |
15 | 25 |
|
16 | 26 | from tests.constants import ( |
@@ -579,16 +589,14 @@ def callback_registry(): |
579 | 589 |
|
580 | 590 | def _configure_testcontainers() -> None: |
581 | 591 | """Configure testcontainers settings for the current platform.""" |
582 | | - try: |
| 592 | + with contextlib.suppress(ImportError): |
583 | 593 | from testcontainers.core.config import testcontainers_config |
584 | 594 |
|
585 | 595 | testcontainers_config.ryuk_disabled = True |
586 | 596 |
|
587 | 597 | # Ensure images are always pulled (don't rely on local cache check) |
588 | 598 | # This fixes issues on some CI environments |
589 | 599 | os.environ.setdefault("TC_IMAGE_PULL_POLICY", "always") |
590 | | - except ImportError: |
591 | | - pass # testcontainers not installed |
592 | 600 |
|
593 | 601 |
|
594 | 602 | def _start_redis_container_with_fallback(): |
@@ -645,7 +653,7 @@ def redis_container() -> Generator[str, None, None]: |
645 | 653 | return |
646 | 654 |
|
647 | 655 | try: |
648 | | - import testcontainers.redis # noqa: F401 |
| 656 | + import testcontainers.redis |
649 | 657 | except ImportError: |
650 | 658 | pytest.skip("testcontainers not installed (pip install testcontainers[redis])") |
651 | 659 | return |
@@ -683,7 +691,7 @@ def redis_container_with_acl() -> Generator[dict, None, None]: |
683 | 691 | - users: Dict of user info (username, password, role) |
684 | 692 | """ |
685 | 693 | try: |
686 | | - import testcontainers.redis # noqa: F401 |
| 694 | + import testcontainers.redis |
687 | 695 | except ImportError: |
688 | 696 | pytest.skip("testcontainers not installed") |
689 | 697 | return |
@@ -868,3 +876,69 @@ def auth_session_manager(mock_oauth_provider, memory_token_store): |
868 | 876 | token_store=memory_token_store, |
869 | 877 | session_key="test_user", |
870 | 878 | ) |
| 879 | + |
| 880 | + |
| 881 | +# ============================================================================= |
| 882 | +# MCP Test Fixtures |
| 883 | +# ============================================================================= |
| 884 | + |
| 885 | + |
| 886 | +@pytest.fixture |
| 887 | +def mcp_fresh_state(): |
| 888 | + """Reset all MCP global state before and after each test. |
| 889 | +
|
| 890 | + Clears the singleton app, widget registry, widget configs, pending |
| 891 | + responses, pending events, and the server-side events bucket. |
| 892 | + """ |
| 893 | + from pywry.mcp import state as mcp_state |
| 894 | + from pywry.mcp.server import _events |
| 895 | + |
| 896 | + mcp_state._app = None |
| 897 | + mcp_state._widgets.clear() |
| 898 | + mcp_state._widget_configs.clear() |
| 899 | + mcp_state._pending_responses.clear() |
| 900 | + mcp_state._pending_events.clear() |
| 901 | + _events.clear() |
| 902 | + yield |
| 903 | + mcp_state._app = None |
| 904 | + mcp_state._widgets.clear() |
| 905 | + mcp_state._widget_configs.clear() |
| 906 | + mcp_state._pending_responses.clear() |
| 907 | + mcp_state._pending_events.clear() |
| 908 | + _events.clear() |
| 909 | + |
| 910 | + |
| 911 | +@pytest.fixture |
| 912 | +def mcp_widget(mcp_fresh_state): |
| 913 | + """Register a single mock widget under id ``w``. |
| 914 | +
|
| 915 | + Depends on ``mcp_fresh_state`` so the registry is clean. |
| 916 | + """ |
| 917 | + from unittest.mock import MagicMock |
| 918 | + |
| 919 | + from pywry.mcp import state as mcp_state |
| 920 | + |
| 921 | + widget = MagicMock() |
| 922 | + widget.widget_id = "w" |
| 923 | + mcp_state._widgets["w"] = widget |
| 924 | + yield widget |
| 925 | + |
| 926 | + |
| 927 | +def make_handler_ctx( |
| 928 | + args: dict[str, Any], |
| 929 | + headless: bool = False, |
| 930 | + events: dict | None = None, |
| 931 | +): |
| 932 | + """Build a HandlerContext for unit-testing MCP handlers. |
| 933 | +
|
| 934 | + The ``make_callback`` is a no-op so tests can focus on the handler |
| 935 | + contract (widget.emit calls + return dict). |
| 936 | + """ |
| 937 | + from pywry.mcp.handlers import HandlerContext |
| 938 | + |
| 939 | + return HandlerContext( |
| 940 | + args=args, |
| 941 | + events=events if events is not None else {}, |
| 942 | + make_callback=lambda _wid: lambda *_a, **_kw: None, |
| 943 | + headless=headless, |
| 944 | + ) |
0 commit comments