refactor: move MCP lifecycle out of AgentLoop (#5343)

This commit is contained in:
chengyongru
2026-08-12 17:51:04 +08:00
committed by GitHub
parent 686dd0603e
commit 19997d20bb
39 changed files with 1192 additions and 846 deletions
+57 -9
View File
@@ -34,8 +34,7 @@ AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def _make_mock_agent(response_text: str = "mock response") -> MagicMock:
agent = MagicMock()
agent.process_direct = AsyncMock(return_value=response_text)
agent._connect_mcp = AsyncMock()
agent.close_mcp = AsyncMock()
agent.aclose = AsyncMock()
agent._last_usage = {"prompt_tokens": 100, "completion_tokens": 50}
return agent
@@ -149,6 +148,59 @@ async def test_api_routes_allow_requests_without_configured_api_key(aiohttp_clie
mock_agent.process_direct.assert_called_once()
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
@pytest.mark.asyncio
async def test_api_prepares_application_resources_before_each_turn(aiohttp_client) -> None:
events: list[str] = []
agent = _make_mock_agent()
async def prepare_agent() -> None:
events.append("prepare")
async def process_direct(**_kwargs):
events.append("process")
return "ready"
agent.process_direct = process_direct
app = create_app(agent, prepare_agent=prepare_agent)
client = await aiohttp_client(app)
response = await client.post(
"/v1/chat/completions",
json={"messages": [{"role": "user", "content": "hello"}]},
)
assert response.status == 200
assert events == ["prepare", "process"]
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
@pytest.mark.asyncio
async def test_api_preparation_is_bounded_by_request_timeout(aiohttp_client) -> None:
agent = _make_mock_agent()
started = asyncio.Event()
async def prepare_agent() -> None:
started.set()
await asyncio.Event().wait()
app = create_app(
agent,
request_timeout=0.01,
prepare_agent=prepare_agent,
)
client = await aiohttp_client(app)
response = await client.post(
"/v1/chat/completions",
json={"messages": [{"role": "user", "content": "hello"}]},
)
assert started.is_set()
assert response.status == 504
agent.process_direct.assert_not_awaited()
@pytest.mark.skipif(not HAS_AIOHTTP, reason="aiohttp not installed")
@pytest.mark.asyncio
async def test_no_user_message_returns_400(aiohttp_client, app) -> None:
@@ -275,8 +327,7 @@ async def test_followup_requests_share_same_session_key(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = fake_process
agent._connect_mcp = AsyncMock()
agent.close_mcp = AsyncMock()
agent.aclose = AsyncMock()
agent._last_usage = {}
app = create_app(agent, model_name="m", api_key=API_KEY)
@@ -315,8 +366,7 @@ async def test_fixed_session_requests_are_serialized(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = slow_process
agent._connect_mcp = AsyncMock()
agent.close_mcp = AsyncMock()
agent.aclose = AsyncMock()
agent._last_usage = {}
app = create_app(agent, model_name="m", api_key=API_KEY)
@@ -433,8 +483,7 @@ async def test_empty_response_falls_back_without_retry(aiohttp_client) -> None:
agent = MagicMock()
agent.process_direct = always_empty
agent._connect_mcp = AsyncMock()
agent.close_mcp = AsyncMock()
agent.aclose = AsyncMock()
agent._last_usage = {}
app = create_app(agent, model_name="m", api_key=API_KEY)
@@ -457,7 +506,6 @@ async def test_process_direct_accepts_media() -> None:
from nanobot.bus.runtime_events import RuntimeEventPublisher
loop = AgentLoop.__new__(AgentLoop)
loop._connect_mcp = AsyncMock()
loop._session_locks = {}
loop.runtime_event_publisher = RuntimeEventPublisher()