hermes-agent/tests/tools/test_mcp_cancelled_error_propagation.py
kshitijk4poor 66827f8947 chore: prune unused imports and duplicate import redefinitions
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.

- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
  public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
  unused in their defining module are kept with explicit # noqa:
  F401 (gateway/run.py load_dotenv; run_agent re-exports from
  agent.message_sanitization, agent.context_compressor,
  agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
  agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
  can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
  selected); this is a one-time cleanup, not a config change

Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
  toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
  module still resolves
2026-05-28 22:26:25 -07:00

91 lines
3.8 KiB
Python

"""Regression tests for ``MCPServerTask.run`` + ``asyncio.CancelledError``.
Background
==========
On Python 3.11+, ``asyncio.CancelledError`` inherits from ``BaseException``
rather than ``Exception``, so a bare ``except Exception`` does NOT catch it.
``MCPServerTask.run`` had a broad ``except Exception`` around the transport
loop which meant a task cancellation (gateway restart, explicit
``task.cancel()``) caused the reconnect loop to exit silently — the MCP
server stayed dead until Hermes was restarted. See #9930.
The fix adds an explicit ``except asyncio.CancelledError: raise`` BEFORE
the broad catch so cancellation propagates cleanly to asyncio's task
machinery and ``MCPServerTask.shutdown()``'s ``await self._task`` completes
without hanging the reconnect loop.
"""
from __future__ import annotations
import asyncio
from unittest.mock import patch
async def _hanging_run(self, cfg):
"""Stand-in transport that hangs forever so we can cancel it."""
await asyncio.sleep(3600)
class TestCancelledErrorPropagation:
def test_cancelled_error_is_not_swallowed_by_except_exception(self):
"""CancelledError raised inside the transport call must re-raise
so the reconnect loop terminates cleanly on cancel — not stay wedged."""
from tools.mcp_tool import MCPServerTask
server = MCPServerTask("cancel-test")
async def drive():
with patch.object(MCPServerTask, "_run_stdio", _hanging_run), \
patch.object(MCPServerTask, "_is_http", lambda self: False):
task = asyncio.create_task(server.run({"command": "fake"}))
# Let the run loop enter the try/except and start awaiting.
await asyncio.sleep(0.05)
task.cancel()
# The fix guarantees the task completes (either via
# CancelledError propagation or clean exit) rather than
# hanging forever.
try:
await asyncio.wait_for(task, timeout=2.0)
except asyncio.CancelledError:
return "cancelled_cleanly"
except asyncio.TimeoutError:
# If we hit this, the reconnect loop swallowed the cancel
# and stayed wedged — the exact #9930 bug.
task.cancel()
try:
await task
except Exception:
pass
return "wedged"
return "clean_return"
outcome = asyncio.run(drive())
assert outcome in {"cancelled_cleanly", "clean_return"}, (
f"MCPServerTask.run wedged on cancel (outcome={outcome}) — "
f"#9930 regression"
)
def test_shutdown_completes_promptly_when_task_is_cancelled(self):
"""``shutdown()`` falls through to ``task.cancel()`` + ``await self._task``
after a grace period. That cancel must unwedge the reconnect loop —
otherwise ``await self._task`` hangs indefinitely."""
from tools.mcp_tool import MCPServerTask
server = MCPServerTask("shutdown-cancel-test")
async def drive():
with patch.object(MCPServerTask, "_run_stdio", _hanging_run), \
patch.object(MCPServerTask, "_is_http", lambda self: False):
server._task = asyncio.ensure_future(server.run({"command": "fake"}))
await asyncio.sleep(0.05)
server._shutdown_event.set()
server._task.cancel()
try:
await asyncio.wait_for(server._task, timeout=2.0)
except (asyncio.CancelledError, asyncio.TimeoutError):
pass
return server._task.done()
done = asyncio.run(drive())
assert done, "MCPServerTask did not finish after cancel — #9930 regression"