fix(mcp): harden nested interruption detection
This commit is contained in:
parent
80209e51db
commit
1ae1dd8b2c
|
|
@ -121,6 +121,39 @@ def test_is_session_expired_rejects_mixed_group_with_user_interruption():
|
|||
assert _is_session_expired_error(exc) is False
|
||||
|
||||
|
||||
def test_exception_tree_finds_interruption_beyond_recursion_limit():
|
||||
"""Arbitrarily deep wrapper trees must not overflow Python's call stack."""
|
||||
import sys
|
||||
|
||||
from tools.mcp_tool import _exception_tree_contains_interruption
|
||||
|
||||
class NestedException(Exception):
|
||||
exceptions: tuple[BaseException, ...]
|
||||
|
||||
exc = InterruptedError("cancel")
|
||||
for _ in range(sys.getrecursionlimit() + 100):
|
||||
wrapper = NestedException("wrapped")
|
||||
wrapper.exceptions = (exc,)
|
||||
exc = wrapper
|
||||
|
||||
assert _exception_tree_contains_interruption(exc) is True
|
||||
|
||||
|
||||
def test_exception_tree_handles_cyclic_exceptions_graph():
|
||||
"""Malformed exception graphs may contain cycles and must terminate safely."""
|
||||
from tools.mcp_tool import _exception_tree_contains_interruption
|
||||
|
||||
class CyclicException(Exception):
|
||||
exceptions: tuple[BaseException, ...]
|
||||
|
||||
first = CyclicException("first")
|
||||
second = CyclicException("second")
|
||||
first.exceptions = (second,)
|
||||
second.exceptions = (first,)
|
||||
|
||||
assert _exception_tree_contains_interruption(first) is False
|
||||
|
||||
|
||||
def test_is_session_expired_rejects_empty_message():
|
||||
"""Bare exceptions with no message shouldn't match."""
|
||||
from tools.mcp_tool import _is_session_expired_error
|
||||
|
|
|
|||
|
|
@ -3902,12 +3902,18 @@ _SESSION_EXPIRED_MARKERS: tuple = (
|
|||
|
||||
def _exception_tree_contains_interruption(exc: BaseException) -> bool:
|
||||
"""Return whether ``exc`` or any nested exception is user cancellation."""
|
||||
if isinstance(exc, InterruptedError):
|
||||
return True
|
||||
return any(
|
||||
_exception_tree_contains_interruption(child)
|
||||
for child in getattr(exc, "exceptions", ())
|
||||
)
|
||||
stack = [exc]
|
||||
seen: set[int] = set()
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
identity = id(current)
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
if isinstance(current, InterruptedError):
|
||||
return True
|
||||
stack.extend(getattr(current, "exceptions", ()))
|
||||
return False
|
||||
|
||||
|
||||
def _is_session_expired_error(exc: BaseException) -> bool:
|
||||
|
|
|
|||
Loading…
Reference in New Issue