|
| 1 | +import datetime |
| 2 | +import json |
| 3 | +from types import SimpleNamespace |
| 4 | + |
| 5 | +import pytest |
| 6 | + |
| 7 | +from telegram_mcp.tools import chats |
| 8 | + |
| 9 | +# Distinct sentinel returned by get_input_entity so the test can assert the |
| 10 | +# GetPeerDialogsRequest was built for exactly the resolved peer. |
| 11 | +FAKE_INPUT_PEER = object() |
| 12 | + |
| 13 | + |
| 14 | +class FakeChatClient: |
| 15 | + """Client stub whose per-peer methods let us assert get_chat reads the |
| 16 | + REQUESTED chat's dialog, not the account's top dialog. |
| 17 | +
|
| 18 | + Regression guard: get_chat used get_dialogs(limit=1, offset_peer=entity), |
| 19 | + where Telethon's `offset_peer` is a pagination cursor (not a filter). With |
| 20 | + offset_id=0 it is effectively ignored, so limit=1 returned the account's |
| 21 | + top dialog and mis-attributed its unread/archived/last_message to the |
| 22 | + requested chat. |
| 23 | + """ |
| 24 | + |
| 25 | + def __init__(self, entity, last_message, *, unread=0, folder_id=0): |
| 26 | + self.entity = entity |
| 27 | + self._last_message = last_message |
| 28 | + self._unread = unread |
| 29 | + self._folder_id = folder_id |
| 30 | + self.peer_dialog_peers = None |
| 31 | + self.get_messages_calls = [] |
| 32 | + self.get_dialogs_called = False |
| 33 | + |
| 34 | + async def get_participants(self, entity, limit=0): |
| 35 | + return SimpleNamespace(total=9) |
| 36 | + |
| 37 | + async def get_input_entity(self, entity): |
| 38 | + assert entity is self.entity |
| 39 | + return FAKE_INPUT_PEER |
| 40 | + |
| 41 | + async def __call__(self, request): |
| 42 | + # functions.messages.GetPeerDialogsRequest for exactly the requested peer |
| 43 | + self.peer_dialog_peers = [p.peer for p in request.peers] |
| 44 | + return SimpleNamespace( |
| 45 | + dialogs=[SimpleNamespace(unread_count=self._unread, folder_id=self._folder_id)] |
| 46 | + ) |
| 47 | + |
| 48 | + async def get_messages(self, entity, limit=1): |
| 49 | + self.get_messages_calls.append({"entity": entity, "limit": limit}) |
| 50 | + return [self._last_message] |
| 51 | + |
| 52 | + async def get_dialogs(self, *args, **kwargs): |
| 53 | + self.get_dialogs_called = True |
| 54 | + raise AssertionError( |
| 55 | + "get_chat must not use get_dialogs(offset_peer=...) — it returns the " |
| 56 | + "account's top dialog, not the requested chat" |
| 57 | + ) |
| 58 | + |
| 59 | + |
| 60 | +def _async_return(value): |
| 61 | + async def _inner(*args, **kwargs): |
| 62 | + return value |
| 63 | + |
| 64 | + return _inner |
| 65 | + |
| 66 | + |
| 67 | +def _patch(monkeypatch, client, entity): |
| 68 | + monkeypatch.setattr(chats, "get_client", lambda account=None: client) |
| 69 | + monkeypatch.setattr(chats, "resolve_entity", _async_return(entity)) |
| 70 | + monkeypatch.setattr(chats, "get_marked_id", lambda e: -1002929916934) |
| 71 | + monkeypatch.setattr(chats, "get_entity_type", lambda e: "Supergroup") |
| 72 | + |
| 73 | + |
| 74 | +def _parse(result): |
| 75 | + return json.loads(result.split("\n\n")[0]) |
| 76 | + |
| 77 | + |
| 78 | +@pytest.mark.asyncio |
| 79 | +async def test_get_chat_reads_requested_peer_not_top_dialog(monkeypatch): |
| 80 | + entity = SimpleNamespace(title="Технический Мониторинг", username=None) |
| 81 | + last_msg = SimpleNamespace( |
| 82 | + date=datetime.datetime(2026, 7, 19, 1, 0, 0, tzinfo=datetime.timezone.utc), |
| 83 | + message="NetBird mesh alert", |
| 84 | + sender=SimpleNamespace(first_name="cobalt_quartz_bot", last_name=None, title=None), |
| 85 | + ) |
| 86 | + client = FakeChatClient(entity, last_msg, unread=5, folder_id=0) |
| 87 | + _patch(monkeypatch, client, entity) |
| 88 | + |
| 89 | + result = await chats.get_chat(chat_id=-1002929916934, account=None) |
| 90 | + payload = _parse(result) |
| 91 | + |
| 92 | + assert "GEN-ERR" not in result |
| 93 | + # last_message comes from the requested chat, not a foreign top dialog |
| 94 | + assert payload["last_message"]["sender"] == "cobalt_quartz_bot" |
| 95 | + assert payload["last_message"]["text"] == "NetBird mesh alert" |
| 96 | + assert payload["unread"] == 5 |
| 97 | + assert payload["archived"] is False |
| 98 | + # resolved via GetPeerDialogsRequest for exactly this peer, never get_dialogs |
| 99 | + assert client.peer_dialog_peers == [FAKE_INPUT_PEER] |
| 100 | + assert client.get_messages_calls == [{"entity": entity, "limit": 1}] |
| 101 | + assert client.get_dialogs_called is False |
| 102 | + |
| 103 | + |
| 104 | +@pytest.mark.asyncio |
| 105 | +async def test_get_chat_marks_archived_from_folder_id(monkeypatch): |
| 106 | + entity = SimpleNamespace(title="Archived Group", username=None) |
| 107 | + last_msg = SimpleNamespace( |
| 108 | + date=datetime.datetime(2026, 7, 19, 1, 0, 0, tzinfo=datetime.timezone.utc), |
| 109 | + message="hi", |
| 110 | + sender=SimpleNamespace(first_name="Ada", last_name="Lovelace", title=None), |
| 111 | + ) |
| 112 | + client = FakeChatClient(entity, last_msg, unread=0, folder_id=1) |
| 113 | + _patch(monkeypatch, client, entity) |
| 114 | + |
| 115 | + payload = _parse(await chats.get_chat(chat_id=-100777, account=None)) |
| 116 | + |
| 117 | + assert payload["archived"] is True |
| 118 | + assert payload["last_message"]["sender"] == "Ada Lovelace" |
0 commit comments