fix: refresh stale Telegram DM topic threads

(cherry picked from commit 26b87057ad)
This commit is contained in:
stepanov1975 2026-05-16 20:01:40 +00:00 committed by Teknium
parent dcd504cea4
commit c394e7919d
5 changed files with 202 additions and 14 deletions

View file

@ -134,11 +134,31 @@ class RecordingAdapter:
self.calls.append({"chat_id": chat_id, "content": content, "metadata": metadata})
return {"success": True}
async def ensure_dm_topic(self, chat_id, topic_name):
self.ensure_dm_topic_calls.append({"chat_id": chat_id, "topic_name": topic_name})
async def ensure_dm_topic(self, chat_id, topic_name, force_create=False):
self.ensure_dm_topic_calls.append(
{"chat_id": chat_id, "topic_name": topic_name, "force_create": force_create}
)
return "38049"
class StaleTopicAdapter:
def __init__(self):
self.calls = []
self.ensure_dm_topic_calls = []
async def send(self, chat_id, content, metadata=None):
self.calls.append({"chat_id": chat_id, "content": content, "metadata": dict(metadata or {})})
if len(self.calls) == 1:
return SendResult(success=False, error="Bad Request: message thread not found")
return SendResult(success=True, message_id="fresh-message")
async def ensure_dm_topic(self, chat_id, topic_name, force_create=False):
self.ensure_dm_topic_calls.append(
{"chat_id": chat_id, "topic_name": topic_name, "force_create": force_create}
)
return "38064" if force_create else "32343"
@pytest.mark.asyncio
async def test_explicit_telegram_private_thread_requires_reply_anchor(tmp_path, monkeypatch):
monkeypatch.setattr("gateway.delivery.get_hermes_home", lambda: tmp_path)
@ -162,7 +182,7 @@ async def test_named_telegram_private_topic_is_created_before_delivery(tmp_path,
await router._deliver_to_platform(target, "hello", metadata=None)
assert adapter.ensure_dm_topic_calls == [
{"chat_id": "722341991", "topic_name": "Hermes API Test"}
{"chat_id": "722341991", "topic_name": "Hermes API Test", "force_create": False}
]
assert adapter.calls == [
{
@ -176,6 +196,24 @@ async def test_named_telegram_private_topic_is_created_before_delivery(tmp_path,
]
@pytest.mark.asyncio
async def test_named_telegram_private_topic_refreshes_stale_thread_id(tmp_path, monkeypatch):
monkeypatch.setattr("gateway.delivery.get_hermes_home", lambda: tmp_path)
adapter = StaleTopicAdapter()
router = DeliveryRouter(GatewayConfig(), adapters={Platform.TELEGRAM: adapter})
target = DeliveryTarget.parse("telegram:722341991:Personal")
result = await router._deliver_to_platform(target, "hello", metadata=None)
assert getattr(result, "message_id", None) == "fresh-message"
assert adapter.ensure_dm_topic_calls == [
{"chat_id": "722341991", "topic_name": "Personal", "force_create": False},
{"chat_id": "722341991", "topic_name": "Personal", "force_create": True},
]
assert [call["metadata"]["thread_id"] for call in adapter.calls] == ["32343", "38064"]
assert all(call["metadata"]["telegram_dm_topic_created_for_send"] is True for call in adapter.calls)
@pytest.mark.asyncio
async def test_explicit_telegram_private_thread_uses_reply_fallback_with_anchor(tmp_path, monkeypatch):
monkeypatch.setattr("gateway.delivery.get_hermes_home", lambda: tmp_path)

View file

@ -224,7 +224,33 @@ async def test_ensure_dm_topic_creates_on_demand_and_persists():
assert adapter._dm_topics_config == [
{"chat_id": 111, "topics": [{"name": "On Demand", "thread_id": 444}]}
]
adapter._persist_dm_topic_thread_id.assert_called_once_with(111, "On Demand", 444)
adapter._persist_dm_topic_thread_id.assert_called_once_with(
111, "On Demand", 444, replace_existing=False
)
@pytest.mark.asyncio
async def test_ensure_dm_topic_force_create_replaces_persisted_thread_id():
"""Refreshing a stale named topic should replace the cached persisted thread_id."""
adapter = _make_adapter()
bot = AsyncMock()
bot.create_forum_topic.return_value = SimpleNamespace(message_thread_id=777)
adapter._bot = bot
adapter._persist_dm_topic_thread_id = MagicMock()
adapter._dm_topics = {"111:General": 500}
adapter._dm_topics_config = [
{"chat_id": 111, "topics": [{"name": "General", "thread_id": 500}]}
]
result = await adapter.ensure_dm_topic("111", "General", force_create=True)
assert result == "777"
bot.create_forum_topic.assert_called_once_with(chat_id=111, name="General")
assert adapter._dm_topics["111:General"] == 777
assert adapter._dm_topics_config[0]["topics"][0]["thread_id"] == 777
adapter._persist_dm_topic_thread_id.assert_called_once_with(
111, "General", 777, replace_existing=True
)
# ── _persist_dm_topic_thread_id ──
@ -309,6 +335,45 @@ def test_persist_dm_topic_thread_id_skips_if_already_set(tmp_path):
assert topics[0]["thread_id"] == 500 # unchanged
def test_persist_dm_topic_thread_id_replaces_existing_when_requested(tmp_path):
"""Forced refresh should overwrite a stale persisted thread_id."""
import yaml
config_data = {
"platforms": {
"telegram": {
"extra": {
"dm_topics": [
{
"chat_id": 111,
"topics": [
{"name": "General", "icon_color": 123, "thread_id": 500},
],
}
]
}
}
}
}
config_file = tmp_path / ".hermes" / "config.yaml"
config_file.parent.mkdir(parents=True)
with open(config_file, "w") as f:
yaml.dump(config_data, f)
adapter = _make_adapter()
with patch.object(Path, "home", return_value=tmp_path), \
patch.dict(os.environ, {"HERMES_HOME": str(tmp_path / ".hermes")}):
adapter._persist_dm_topic_thread_id(111, "General", 999, replace_existing=True)
with open(config_file) as f:
result = yaml.safe_load(f)
topics = result["platforms"]["telegram"]["extra"]["dm_topics"][0]["topics"]
assert topics[0]["thread_id"] == 999
# ── _get_dm_topic_info ──

View file

@ -624,6 +624,33 @@ async def test_send_created_private_topic_uses_message_thread_without_anchor():
assert "direct_messages_topic_id" not in call_log[0]
@pytest.mark.asyncio
async def test_created_private_topic_thread_not_found_fails_without_root_fallback():
"""Created private-topic sends must not retry into All Messages on stale thread IDs."""
adapter = _make_adapter()
call_log = []
async def mock_send_message(**kwargs):
call_log.append(dict(kwargs))
raise FakeBadRequest("Message thread not found")
adapter._bot = SimpleNamespace(send_message=mock_send_message)
result = await adapter.send(
chat_id="123",
content="created topic message",
metadata={
"thread_id": "32343",
"telegram_dm_topic_created_for_send": True,
},
)
assert result.success is False
assert "thread not found" in str(result.error).lower()
assert len(call_log) == 1
assert call_log[0]["message_thread_id"] == 32343
@pytest.mark.asyncio
async def test_send_uses_metadata_reply_fallback_for_streaming_dm_topics():
"""Metadata-only sends still stay in Hermes-created Telegram DM topics."""