|
16 | 16 | RequestUsage, |
17 | 17 | UserMessage, |
18 | 18 | ) |
19 | | -from autogen_ext.tools.mcp import StdioServerParams |
| 19 | +from autogen_ext.tools.mcp import StdioServerParams, StreamableHttpServerParams |
20 | 20 | from autogen_ext.tools.mcp._actor import ( |
21 | 21 | McpSessionActor, |
22 | 22 | _parse_sampling_content, # pyright: ignore[reportPrivateUsage] |
@@ -553,6 +553,90 @@ async def test_run_actor_session_exception() -> None: |
553 | 553 | assert actor._actor_task is None # type: ignore[reportPrivateUsage] |
554 | 554 |
|
555 | 555 |
|
| 556 | +@pytest.mark.asyncio |
| 557 | +async def test_run_actor_drains_queue_on_session_exception() -> None: |
| 558 | + """Ensure pending command futures are failed when session creation raises. |
| 559 | +
|
| 560 | + Uses StreamableHttpServerParams with an invalid URL to trigger failure, |
| 561 | + covering the queue-draining logic added in the referenced commit. |
| 562 | + """ |
| 563 | + # Use an invalid local URL/port to force immediate connection failure |
| 564 | + server_params = StreamableHttpServerParams( |
| 565 | + url="http://127.0.0.1:1/invalid", # very likely closed port |
| 566 | + timeout=0.1, |
| 567 | + sse_read_timeout=0.1, |
| 568 | + ) |
| 569 | + actor = McpSessionActor(server_params) |
| 570 | + |
| 571 | + # Prepare pending commands before the actor starts, so the outer except drains them |
| 572 | + fut1: asyncio.Future[Any] = asyncio.Future() |
| 573 | + fut2: asyncio.Future[Any] = asyncio.Future() |
| 574 | + await actor._command_queue.put({"type": "list_tools", "future": fut1}) # type: ignore[reportPrivateUsage] |
| 575 | + await actor._command_queue.put({"type": "call_tool", "name": "t", "args": {}, "future": fut2}) # type: ignore[reportPrivateUsage] |
| 576 | + |
| 577 | + actor._active = True # type: ignore[reportPrivateUsage] |
| 578 | + task = asyncio.create_task(actor._run_actor()) # type: ignore[reportPrivateUsage] |
| 579 | + |
| 580 | + # Wait for task to complete; it should handle the exception and drain the queue |
| 581 | + try: |
| 582 | + await asyncio.wait_for(task, timeout=2.0) |
| 583 | + except asyncio.TimeoutError: |
| 584 | + # If something goes wrong, ensure task cleanup for test stability |
| 585 | + task.cancel() |
| 586 | + with pytest.raises(asyncio.CancelledError): |
| 587 | + await task |
| 588 | + |
| 589 | + # Verify futures were failed by the draining logic |
| 590 | + assert fut1.done() |
| 591 | + assert fut1.exception() is not None |
| 592 | + |
| 593 | + assert fut2.done() |
| 594 | + assert fut2.exception() is not None |
| 595 | + |
| 596 | + |
| 597 | +@pytest.mark.asyncio |
| 598 | +async def test_run_actor_draining_swallows_internal_errors() -> None: |
| 599 | + """draining errors during exception handling are swallowed. |
| 600 | +
|
| 601 | + We force `create_mcp_server_session` to raise so `_run_actor` enters the outer |
| 602 | + exception handler, then make `get_nowait()` itself raise a non-QueueEmpty |
| 603 | + exception. The inner `except Exception: pass` (best-effort draining) should |
| 604 | + swallow it and continue to set the shutdown future exception instead of |
| 605 | + crashing the task. |
| 606 | + """ |
| 607 | + actor = McpSessionActor(StdioServerParams(command="echo", args=["test"])) |
| 608 | + |
| 609 | + # Replace the command queue with a mock that raises from get_nowait() |
| 610 | + mock_q = MagicMock() |
| 611 | + mock_q.get_nowait.side_effect = RuntimeError("drain failure") |
| 612 | + actor._command_queue = mock_q # type: ignore[reportPrivateUsage] |
| 613 | + |
| 614 | + # Prepare a shutdown future to observe behavior after draining attempt |
| 615 | + actor._shutdown_future = asyncio.Future() # type: ignore[reportPrivateUsage] |
| 616 | + |
| 617 | + with patch( |
| 618 | + "autogen_ext.tools.mcp._actor.create_mcp_server_session", |
| 619 | + side_effect=Exception("Session error"), |
| 620 | + ): |
| 621 | + actor._active = True # type: ignore[reportPrivateUsage] |
| 622 | + task = asyncio.create_task(actor._run_actor()) # type: ignore[reportPrivateUsage] |
| 623 | + |
| 624 | + # The task should finish and set the shutdown future with the session error |
| 625 | + try: |
| 626 | + await asyncio.wait_for(task, timeout=1.0) |
| 627 | + except asyncio.TimeoutError: |
| 628 | + task.cancel() |
| 629 | + with pytest.raises(asyncio.CancelledError): |
| 630 | + await task |
| 631 | + |
| 632 | + # Draining raised internally, but should have been swallowed (lines 274-276) |
| 633 | + mock_q.get_nowait.assert_called() # type: ignore[reportPrivateUsage] |
| 634 | + assert actor._shutdown_future.done() # type: ignore[reportPrivateUsage] |
| 635 | + exc = actor._shutdown_future.exception() # type: ignore[reportPrivateUsage] |
| 636 | + assert isinstance(exc, Exception) |
| 637 | + assert "Session error" in str(exc) |
| 638 | + |
| 639 | + |
556 | 640 | @pytest.mark.asyncio |
557 | 641 | async def test_run_actor_shutdown_future_exception() -> None: |
558 | 642 | """Test _run_actor sets exception on shutdown future when session fails.""" |
|
0 commit comments