diff --git a/backend/app/mcp/tools.py b/backend/app/mcp/tools.py index 6830b7a..0e1eb8f 100644 --- a/backend/app/mcp/tools.py +++ b/backend/app/mcp/tools.py @@ -68,7 +68,10 @@ return {"error": exc.code, "detail": exc.detail} except errors.FloodWaitError as exc: return {"error": 429, "detail": f"telegram flood wait: {exc.seconds}s — retry later"} - except errors.EntityNotFoundError: + except (errors.PeerIdInvalidError, errors.ChannelInvalidError, + errors.ChannelPrivateError, ValueError): + # EntityNotFoundError у Telethon нет: «диалог не найден» приходит как + # Peer/ChannelInvalid RPC или ValueError из get_input_entity return {"error": 404, "detail": "chat/dialog not found for this account (resolve id via dialogs_list)"} except Exception as exc: # noqa: BLE001 — ошибка тулу как данные, не как падение return {"error": 500, "detail": f"{type(exc).__name__}: {exc}"} diff --git a/backend/tests/test_tool_boundary.py b/backend/tests/test_tool_boundary.py new file mode 100644 index 0000000..86ed3e1 --- /dev/null +++ b/backend/tests/test_tool_boundary.py @@ -0,0 +1,64 @@ +"""Граница `_tool` должна возвращать ошибку данными, а не падать сама. + +Регресс: в Telethon 1.45 нет `EntityNotFoundError` — старый +`except errors.EntityNotFoundError` поднимал AttributeError в момент +вычисления except-клаузы, и любой неожиданный баг тулов уходил на агент как +«module telethon.errors has no attribute ...» вместо нормального error-конверта. + +Тестам не нужен Telethon-клиент — только модуль errors и сам tools: тул-фабрика +не вызывается, проверяется прямо `_tool` (импорт tools безопасен при пустой +конфигурации, т.к. manager резолвится внутри вызовов). +""" + +import asyncio +import sys +from pathlib import Path + +BACKEND = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(BACKEND)) + +import telethon.errors + +from app.mcp import tools +from app.tg.login_flow import DomainError + + +def test_tool_boundary_maps_errors(): + # DomainError → свой код/детайл + async def dom(): + raise DomainError(404, "нет аккаунтов") + + out = asyncio.run(tools._tool(dom)) + assert out == {"error": 404, "detail": "нет аккаунтов"} + + +def test_tool_boundary_entity_not_found_variants(): + """Telethon-объектные «диалог не найден» → 404-данные, не AttributeError.""" + + async def peer(): + raise telethon.errors.PeerIdInvalidError(request=object()) + + out = asyncio.run(tools._tool(peer)) + assert out["error"] == 404 + + async def chan(): + raise telethon.errors.ChannelPrivateError(request=object()) + + out = asyncio.run(tools._tool(chan)) + assert out["error"] == 404 + + # get_input_entity без кеша поднимает ValueError — тоже 404 + async def unresolved(): + raise ValueError("Could not find the input entity for PeerUser(user_id=7)") + + out = asyncio.run(tools._tool(unresolved)) + assert out["error"] == 404 + + +def test_tool_boundary_unknown_exception_is_500_data(): + async def boom(): + raise KeyError("x") + + out = asyncio.run(tools._tool(boom)) + assert out["error"] == 500 + assert "KeyError" in out["detail"] \ No newline at end of file