mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Second, deeper pass over tools/gateway/hermes_cli plus first pass over the trees wave 1 missed (acp, acp_adapter, skills, computer_use, docker, dashboard, conformance, monitoring, secret_sources, hermes_state, providers). Same rubric as wave 1 (AGENTS.md test policy); security, alternation/caching invariants, issue-number regressions, and E2E kept. Real test-quality fixes found and rooted out along the way: - tests/tools/test_command_guards.py made real auxiliary-LLM HTTPS calls (DEFAULT_CONFIG smart-approval leaked in) — pinned approval mode=manual via autouse fixture: 17.4s → 0.4s. - test_model_switch_custom_providers.py / test_user_providers_model_switch.py silently probed live provider catalogs (~2s/test) — stubbed cached_provider_model_ids/provider_model_ids/fetch_api_models. - test_telegram_noise_filter.py: 15-platform copy-paste matrix over shared gateway.run logic → 3 representative platforms (55s → 3.9s). - test_gateway_shutdown.py: stop()'s 5s interrupt-deadline loop spun on MagicMock agents — interrupt.side_effect now clears _running_agents (22s → 1.0s). - test_gateway_inactivity_timeout.py poll-harness timings shrunk 3-5x (24s → 1.1s); test_mcp_stability.py backoff/SIGTERM-grace sleeps patched (15.4s → 2.5s); test_async_delegation.py negative-drain wait 5s → 0.5s. - test_telegram_init_deadline.py: loop-block margin restored to 1.0s with rationale comment — the watchdog-dump assertion needs the loop blocked well past deadline+grace under parallel load (flaked once in the 40-worker verification run at a 0.2s margin). Verification: full hermetic suite via scripts/run_tests.sh — 2,438 files, 21,718 tests passed, 0 failed, 293.9s wall. Suite totals vs original baseline: 46,820 → 19,757 test functions (−57.8%), wall 583.5s → 293.9s (−50%), subprocess CPU 13,564s → 11,623s.
121 lines
4.5 KiB
Python
121 lines
4.5 KiB
Python
"""Tests for Telegram bot mention detection (bug #12545).
|
|
|
|
The old implementation used a naive substring check
|
|
(`f"@{bot_username}" in text.lower()`), which incorrectly matched partial
|
|
substrings like 'foo@hermes_bot.example'.
|
|
|
|
Detection now relies entirely on the MessageEntity objects Telegram's server
|
|
emits for real mentions. A bare `@username` substring in message text without
|
|
a corresponding `MENTION` entity is NOT a mention — this correctly ignores
|
|
@handles that appear inside URLs, code blocks, email-like strings, or quoted
|
|
text, because Telegram's parser does not emit mention entities for any of
|
|
those contexts.
|
|
"""
|
|
from types import SimpleNamespace
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from plugins.platforms.telegram.adapter import TelegramAdapter
|
|
|
|
|
|
def _make_adapter():
|
|
adapter = object.__new__(TelegramAdapter)
|
|
adapter.platform = Platform.TELEGRAM
|
|
adapter.config = PlatformConfig(enabled=True, token="***", extra={})
|
|
adapter._bot = SimpleNamespace(id=999, username="hermes_bot")
|
|
return adapter
|
|
|
|
|
|
def _mention_entity(text, mention="@hermes_bot"):
|
|
"""Build a MENTION entity pointing at a literal `@username` in `text`."""
|
|
offset = text.index(mention)
|
|
return SimpleNamespace(type="mention", offset=offset, length=len(mention))
|
|
|
|
|
|
def _text_mention_entity(offset, length, user_id):
|
|
"""Build a TEXT_MENTION entity (used when the target user has no public @handle)."""
|
|
return SimpleNamespace(
|
|
type="text_mention",
|
|
offset=offset,
|
|
length=length,
|
|
user=SimpleNamespace(id=user_id),
|
|
)
|
|
|
|
|
|
def _message(text=None, caption=None, entities=None, caption_entities=None):
|
|
return SimpleNamespace(
|
|
text=text,
|
|
caption=caption,
|
|
entities=entities or [],
|
|
caption_entities=caption_entities or [],
|
|
message_thread_id=None,
|
|
chat=SimpleNamespace(id=-100, type="group"),
|
|
reply_to_message=None,
|
|
)
|
|
|
|
|
|
class TestRealMentionsAreDetected:
|
|
"""A real Telegram mention always comes with a MENTION entity — detect those."""
|
|
|
|
def test_mention_at_start_of_message(self):
|
|
adapter = _make_adapter()
|
|
text = "@hermes_bot hello world"
|
|
msg = _message(text=text, entities=[_mention_entity(text)])
|
|
assert adapter._message_mentions_bot(msg) is True
|
|
|
|
|
|
def test_text_mention_entity_targets_bot(self):
|
|
"""TEXT_MENTION is Telegram's entity type for @FirstName -> user without a public handle."""
|
|
adapter = _make_adapter()
|
|
msg = _message(text="hey you", entities=[_text_mention_entity(4, 3, user_id=999)])
|
|
assert adapter._message_mentions_bot(msg) is True
|
|
|
|
|
|
class TestSubstringFalsePositivesAreRejected:
|
|
"""Bare `@bot_username` substrings without a MENTION entity must NOT match.
|
|
|
|
These are all inputs where the OLD substring check returned True incorrectly.
|
|
A word-boundary regex would still over-match some of these (code blocks,
|
|
URLs). Entity-based detection handles them all correctly because Telegram's
|
|
parser does not emit mention entities for non-mention contexts.
|
|
"""
|
|
|
|
def test_email_like_substring(self):
|
|
"""bug #12545 exact repro: 'foo@hermes_bot.example'."""
|
|
adapter = _make_adapter()
|
|
msg = _message(text="email me at foo@hermes_bot.example")
|
|
assert adapter._message_mentions_bot(msg) is False
|
|
|
|
|
|
def test_substring_inside_url_without_entity(self):
|
|
"""@handle inside a URL produces a URL entity, not a MENTION entity."""
|
|
adapter = _make_adapter()
|
|
msg = _message(text="see https://example.com/@hermes_bot for details")
|
|
assert adapter._message_mentions_bot(msg) is False
|
|
|
|
|
|
def test_email_substring_in_caption(self):
|
|
adapter = _make_adapter()
|
|
msg = _message(caption="foo@hermes_bot.example")
|
|
assert adapter._message_mentions_bot(msg) is False
|
|
|
|
|
|
class TestEntityEdgeCases:
|
|
"""Malformed or mismatched entities should not crash or over-match."""
|
|
|
|
|
|
def test_malformed_entity_with_negative_offset(self):
|
|
adapter = _make_adapter()
|
|
msg = _message(text="@hermes_bot hi",
|
|
entities=[SimpleNamespace(type="mention", offset=-1, length=11)])
|
|
assert adapter._message_mentions_bot(msg) is False
|
|
|
|
|
|
class TestCaseInsensitivity:
|
|
"""Telegram usernames are case-insensitive; the slice-compare normalizes both sides."""
|
|
|
|
def test_uppercase_mention(self):
|
|
adapter = _make_adapter()
|
|
text = "hi @HERMES_BOT"
|
|
msg = _message(text=text, entities=[_mention_entity(text, mention="@HERMES_BOT")])
|
|
assert adapter._message_mentions_bot(msg) is True
|
|
|