Skip to content

Commit ff3c149

Browse files
committed
test: use thread-aware fake to exercise executor path
Address CodeRabbit review: replace AsyncMock with _ThreadAffinityFakeClient that raises unless async methods run in a worker thread.
1 parent 7d17cf0 commit ff3c149

1 file changed

Lines changed: 52 additions & 13 deletions

File tree

lib/crewai/tests/mcp/test_tool_resolver_native.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for MCPToolResolver native (non-AMP) resolution paths."""
22

3+
import threading
34
from unittest.mock import AsyncMock, MagicMock, patch
45

56
import pytest
@@ -99,26 +100,63 @@ def test_unmatched_runtime_error_is_wrapped_not_swallowed(
99100
resolver._resolve_native(http_config)
100101

101102

103+
class _ThreadAffinityFakeClient:
104+
"""Fake MCP client that raises when awaited from the main thread.
105+
106+
Simulates the thread-affinity constraint of real asyncio objects:
107+
async methods must run in a worker thread, not on the main thread
108+
where an event loop is already running.
109+
"""
110+
111+
def __init__(self, *args, **kwargs):
112+
self._main_tid = threading.current_thread().ident
113+
self.connected = False
114+
self._executor_tid = None
115+
116+
def _check_affinity(self):
117+
if (
118+
threading.current_thread().ident == self._main_tid
119+
and _loop_is_running()
120+
):
121+
raise RuntimeError(
122+
"Event loop affinity: async operations must run in a worker thread"
123+
)
124+
125+
async def connect(self):
126+
self._check_affinity()
127+
self._executor_tid = threading.current_thread().ident
128+
self.connected = True
129+
130+
async def disconnect(self):
131+
self._check_affinity()
132+
self.connected = False
133+
134+
async def list_tools(self):
135+
self._check_affinity()
136+
return [{"name": "test_tool", "description": "A test tool", "inputSchema": {}}]
137+
138+
139+
def _loop_is_running():
140+
try:
141+
import asyncio
142+
asyncio.get_running_loop()
143+
return True
144+
except RuntimeError:
145+
return False
146+
147+
102148
class TestResolveNativeAsyncioThreadAffinity:
103-
@patch("crewai.mcp.tool_resolver.MCPClient")
149+
@patch("crewai.mcp.tool_resolver.MCPClient", _ThreadAffinityFakeClient)
104150
def test_resolve_native_from_running_loop_uses_executor_path(
105-
self, mock_client_class, resolver, http_config
151+
self, resolver, http_config
106152
):
107153
"""Test that _resolve_native works when called from a running event loop.
108154
109-
When an event loop is already running, _resolve_native uses
110-
executor.submit() with a lambda to create the coroutine inside
111-
the worker thread, avoiding the thread affinity error.
155+
Uses a thread-aware fake that raises a thread-affinity error unless
156+
async methods execute in a worker thread (i.e. via the executor path).
112157
"""
113158
import asyncio
114159

115-
mock_client = AsyncMock()
116-
mock_client.list_tools = AsyncMock(return_value=[])
117-
mock_client.connected = False
118-
mock_client.connect = AsyncMock()
119-
mock_client.disconnect = AsyncMock()
120-
mock_client_class.return_value = mock_client
121-
122160
mock_log = MagicMock()
123161
resolver._logger = MagicMock(log=mock_log)
124162

@@ -127,5 +165,6 @@ async def _call_resolve_native():
127165

128166
tools, clients = asyncio.run(_call_resolve_native())
129167

130-
assert tools == []
168+
assert len(tools) == 1
169+
assert tools[0].name == "test_tool"
131170
assert clients == []

0 commit comments

Comments
 (0)