mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-31 06:51:29 +00:00
fix(send_message): route standalone Telegram sends through TELEGRAM_PROXY
When the send_message tool runs outside the gateway process (agent loop, TUI, cron, etc.), _gateway_runner_ref() returns None and the standalone path in _send_telegram constructs Bot(token=token) directly, bypassing any configured proxy. In regions where api.telegram.org is blocked, the send times out after ~5s with 'Telegram send failed: Timed out' and nothing ever shows up in gateway.log because the request never reaches the gateway. Resolve TELEGRAM_PROXY (via gateway.platforms.base.resolve_proxy_url, which also honours HTTPS_PROXY/HTTP_PROXY/ALL_PROXY and NO_PROXY) just before constructing the Bot. When a proxy is found, attach an HTTPXRequest(proxy=...) for both 'request' and 'get_updates_request', matching what gateway/platforms/telegram.py already does for in-gateway sends and what the Discord standalone sender already does. Any exception attaching the proxy falls back cleanly to a direct connection, preserving prior behaviour for users without a proxy configured. Adds tests/tools/test_send_message_telegram_proxy.py covering both the proxy-configured and no-proxy cases.
This commit is contained in:
parent
785993bcae
commit
edce8a5fd4
2 changed files with 178 additions and 1 deletions
154
tests/tools/test_send_message_telegram_proxy.py
Normal file
154
tests/tools/test_send_message_telegram_proxy.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
"""Regression tests for the standalone Telegram send path's proxy support.
|
||||
|
||||
The ``send_message`` tool, when invoked from a process *other than* the
|
||||
gateway (agent / TUI / cron), runs ``_send_telegram`` directly instead of
|
||||
delegating to the in-process gateway adapter. Before the fix that
|
||||
accompanies these tests, that standalone path constructed
|
||||
``telegram.Bot(token=...)`` with no proxy, so in regions where
|
||||
api.telegram.org is blocked (e.g. RU) the send would just time out with
|
||||
``Telegram send failed: Timed out`` and never show up in ``gateway.log``.
|
||||
|
||||
These tests verify that the standalone path now honours ``TELEGRAM_PROXY``
|
||||
the same way the gateway adapter (and the Discord standalone path) do.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _install_telegram_mock_with_request(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
bot_factory: MagicMock,
|
||||
httpx_request_factory: MagicMock,
|
||||
) -> None:
|
||||
"""Install a stub ``telegram`` package whose ``Bot`` and
|
||||
``telegram.request.HTTPXRequest`` are the supplied mocks.
|
||||
|
||||
Mirrors ``_install_telegram_mock`` in test_send_message_tool.py but also
|
||||
provides the ``telegram.request`` submodule that the proxy branch needs.
|
||||
"""
|
||||
parse_mode = SimpleNamespace(MARKDOWN_V2="MarkdownV2", HTML="HTML")
|
||||
constants_mod = SimpleNamespace(ParseMode=parse_mode)
|
||||
request_mod = SimpleNamespace(HTTPXRequest=httpx_request_factory)
|
||||
telegram_mod = SimpleNamespace(
|
||||
Bot=bot_factory,
|
||||
constants=constants_mod,
|
||||
request=request_mod,
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "telegram", telegram_mod)
|
||||
monkeypatch.setitem(sys.modules, "telegram.constants", constants_mod)
|
||||
monkeypatch.setitem(sys.modules, "telegram.request", request_mod)
|
||||
|
||||
|
||||
def _make_bot() -> MagicMock:
|
||||
bot = MagicMock()
|
||||
bot.send_message = AsyncMock(return_value=SimpleNamespace(message_id=42))
|
||||
return bot
|
||||
|
||||
|
||||
class TestSendTelegramStandaloneProxy:
|
||||
"""The standalone ``_send_telegram`` path must route through
|
||||
``TELEGRAM_PROXY`` when one is configured, even when no in-process
|
||||
gateway runner is available.
|
||||
"""
|
||||
|
||||
def test_proxy_env_passed_to_httpx_request(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""With TELEGRAM_PROXY set, Bot() is constructed with HTTPXRequest
|
||||
instances whose ``proxy=`` kwarg is the configured URL — applied to
|
||||
both ``request`` and ``get_updates_request``.
|
||||
"""
|
||||
from tools.send_message_tool import _send_telegram
|
||||
|
||||
proxy_url = "socks5://127.0.0.1:1080"
|
||||
monkeypatch.setenv("TELEGRAM_PROXY", proxy_url)
|
||||
# Clear NO_PROXY so resolve_proxy_url() doesn't short-circuit on
|
||||
# leftover env from the host running the tests.
|
||||
monkeypatch.delenv("NO_PROXY", raising=False)
|
||||
monkeypatch.delenv("no_proxy", raising=False)
|
||||
# Ensure the test does not depend on the in-process gateway runner.
|
||||
monkeypatch.setattr("gateway.run._gateway_runner_ref", lambda: None)
|
||||
|
||||
bot = _make_bot()
|
||||
bot_factory = MagicMock(return_value=bot)
|
||||
httpx_request_factory = MagicMock(side_effect=lambda **kw: MagicMock(_kw=kw))
|
||||
_install_telegram_mock_with_request(monkeypatch, bot_factory, httpx_request_factory)
|
||||
|
||||
result: dict[str, Any] = asyncio.run(
|
||||
_send_telegram("tok", "123", "hello world")
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
bot_factory.assert_called_once()
|
||||
call_kwargs = bot_factory.call_args.kwargs
|
||||
assert call_kwargs.get("token") == "tok"
|
||||
assert "request" in call_kwargs, "request= kwarg missing — proxy not wired"
|
||||
assert "get_updates_request" in call_kwargs, (
|
||||
"get_updates_request= kwarg missing — proxy not wired"
|
||||
)
|
||||
|
||||
# HTTPXRequest must have been invoked twice, both times with the
|
||||
# resolved proxy URL.
|
||||
assert httpx_request_factory.call_count == 2
|
||||
for call in httpx_request_factory.call_args_list:
|
||||
assert call.kwargs.get("proxy") == proxy_url, (
|
||||
f"HTTPXRequest called without proxy={proxy_url!r}: {call.kwargs!r}"
|
||||
)
|
||||
|
||||
# And the bot was actually used to send.
|
||||
bot.send_message.assert_awaited_once()
|
||||
|
||||
def test_no_proxy_env_uses_plain_bot(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Without TELEGRAM_PROXY (and no inherited HTTPS_PROXY/etc), Bot()
|
||||
is constructed plainly — no ``request``/``get_updates_request``
|
||||
kwargs, and HTTPXRequest is not invoked at all.
|
||||
"""
|
||||
from tools.send_message_tool import _send_telegram
|
||||
|
||||
# Wipe every env var resolve_proxy_url() inspects so the host's
|
||||
# ambient proxy settings can't flip this test green-or-red.
|
||||
for var in (
|
||||
"TELEGRAM_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
):
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
monkeypatch.setattr("gateway.run._gateway_runner_ref", lambda: None)
|
||||
# Make sure macOS system-proxy auto-detection (scutil) can't kick in.
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
|
||||
bot = _make_bot()
|
||||
bot_factory = MagicMock(return_value=bot)
|
||||
httpx_request_factory = MagicMock(side_effect=lambda **kw: MagicMock(_kw=kw))
|
||||
_install_telegram_mock_with_request(monkeypatch, bot_factory, httpx_request_factory)
|
||||
|
||||
result: dict[str, Any] = asyncio.run(
|
||||
_send_telegram("tok", "123", "hello world")
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
bot_factory.assert_called_once()
|
||||
call_kwargs = bot_factory.call_args.kwargs
|
||||
call_args = bot_factory.call_args.args
|
||||
# token may be passed positionally or as a kwarg; either is fine.
|
||||
assert call_kwargs.get("token", call_args[0] if call_args else None) == "tok"
|
||||
assert "request" not in call_kwargs
|
||||
assert "get_updates_request" not in call_kwargs
|
||||
httpx_request_factory.assert_not_called()
|
||||
bot.send_message.assert_awaited_once()
|
||||
|
|
@ -816,7 +816,30 @@ async def _send_telegram(token, chat_id, message, media_files=None, thread_id=No
|
|||
formatted = message
|
||||
send_parse_mode = ParseMode.MARKDOWN_V2
|
||||
|
||||
bot = Bot(token=token)
|
||||
# Honour a configured proxy (telegram.proxy_url in config.yaml, exported
|
||||
# as TELEGRAM_PROXY env var by load_gateway_config). Without this, the
|
||||
# standalone send path bypasses the proxy and times out in regions
|
||||
# where api.telegram.org is blocked. The in-gateway adapter does the
|
||||
# same thing in gateway/platforms/telegram.py.
|
||||
try:
|
||||
from gateway.platforms.base import resolve_proxy_url
|
||||
_tg_proxy = resolve_proxy_url("TELEGRAM_PROXY", target_hosts=["api.telegram.org"])
|
||||
except Exception:
|
||||
_tg_proxy = None
|
||||
if _tg_proxy:
|
||||
try:
|
||||
from telegram.request import HTTPXRequest
|
||||
logger.info("send_message: standalone Telegram send routed through proxy %s", _tg_proxy)
|
||||
bot = Bot(
|
||||
token=token,
|
||||
request=HTTPXRequest(proxy=_tg_proxy),
|
||||
get_updates_request=HTTPXRequest(proxy=_tg_proxy),
|
||||
)
|
||||
except Exception as _proxy_err:
|
||||
logger.warning("send_message: failed to attach Telegram proxy (%s), falling back to direct connection", _proxy_err)
|
||||
bot = Bot(token=token)
|
||||
else:
|
||||
bot = Bot(token=token)
|
||||
int_chat_id = int(chat_id)
|
||||
media_files = media_files or []
|
||||
thread_kwargs = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue