diff --git a/harness/tg_inbox.py b/harness/tg_inbox.py index a778c32..87df8b7 100644 --- a/harness/tg_inbox.py +++ b/harness/tg_inbox.py @@ -264,6 +264,11 @@ def receive_message(update: dict): data = { "update_id": update["update_id"], "chat_id": chat.get("id"), + # t-261013113629: тип чата (private/group/supergroup/channel) и название — + # маршрутизация ответов в чат-источник (группа vs личка) и дефолтный + # чат уведомлений (личка Alex, а не последняя активная группа) + "chat_type": chat.get("type"), + "chat_title": chat.get("title"), # tg message_id нужен для setMessageReaction (реакция «seen», e-261002202857): # update_id ≠ message_id (глобальный стрим vs чат), фолбэк — старый hash "message_id": msg.get("message_id"), @@ -547,7 +552,20 @@ def _default_chat_id(): and not m.get("ignored")] if not inbox: return None # некому: чат ещё не известен - return sorted(inbox, key=lambda m: m.get("ts") or "")[-1]["chat_id"] + # t-261013113629: уведомления/файлы — в ЛИЧКУ Alex, а не в последний + # активный чат (иначе ответ в группе делает её дефолтом, и файлы/дайджесты + # уходят всем участникам группы). Личный чат = private по chat_type; + # старые записи без chat_type — chat_id > 0 (у групп он отрицательный). + def _is_private(m): + if m.get("chat_type"): + return m.get("chat_type") == "private" + try: # старые записи без chat_type: у групп chat_id < 0 + return int(m.get("chat_id")) > 0 + except (TypeError, ValueError): + return False + privates = [m for m in inbox if _is_private(m)] + pool = privates or inbox + return sorted(pool, key=lambda m: m.get("ts") or "")[-1]["chat_id"] def _post_multipart(method: str, fields: dict, file_path: str, diff --git a/tests/test_reply_routing.py b/tests/test_reply_routing.py new file mode 100644 index 0000000..d29b19f --- /dev/null +++ b/tests/test_reply_routing.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +"""t-261013113629: маршрутизация ответов TG — ответ уходит в чат-источник. + +Проверяет: ответ на сообщение из ГРУППЫ уходит в группу, из ЛИЧКИ — в личку; +send_batch_reply разбивает смешанный батч по чатам (не смешивает разговоры); +notify_chat/_default_chat_id выбирает личку Alex, даже если последнее +активное сообщение — из группы (старые записи без chat_type → chat_id<0). + +Запуск: ./venv/bin/python tests/test_reply_routing.py +""" +import os +import sys + +_DST = "/tmp/simbiot-test-reply-routing.db" +for p in (_DST, _DST + "-wal", _DST + "-shm"): + if os.path.exists(p): + os.unlink(p) +os.environ["SIMBIOT_DB"] = _DST +sys.path.insert(0, "/root/simbiot-harness") + +from harness import es, tg_inbox + +fails = [] + + +def check(name, cond, extra=""): + print(("PASS" if cond else "FAIL") + " " + name + (" | " + extra if not cond else "")) + if not cond: + fails.append(name) + + +def _recv(update_id, text, chat_id, chat_type, chat_title=None, date=1780000000): + chat = {"id": chat_id, "type": chat_type} + if chat_title: + chat["title"] = chat_title + tg_inbox.receive_message({"update_id": update_id, "message": { + "message_id": update_id % 100000, "date": date, "chat": chat, + "from": {"id": 1, "username": "alex", "first_name": "Alex"}, "text": text}}) + + +es.init_db() + +PRIVATE_ID = 424242 # личка Alex (>0) +GROUP_ID = -100999 # группа (<0) + +# ── 1. send_reply: ответ в чат-источник (группа → группа, личка → личка) ── +_recv(900, "вопрос в личке", PRIVATE_ID, "private", date=1780000000) +_recv(901, "вопрос в группе", GROUP_ID, "supergroup", "Кооп 42", date=1780000001) + +sends, actions = [], [] +tg_inbox.send_message = lambda chat_id, text, reply_to=None, timeout=35: ( + sends.append((chat_id, text)) or {"ok": True}) +tg_inbox._api = lambda method, params, timeout=35: ( + actions.append((method, params.get("chat_id"))) or {"ok": True}) + +tg_inbox.send_reply("m-900", "ответ в личку") +check("send_reply: личка → личка", sends and sends[-1][0] == PRIVATE_ID, str(sends)) +tg_inbox.send_reply("m-901", "ответ в группу") +check("send_reply: группа → группа", sends and sends[-1][0] == GROUP_ID, str(sends)) +model = es.run_projections() +check("send_reply: оба закрыты (replied)", + model["inbox"]["m-900"]["replied"] and model["inbox"]["m-901"]["replied"]) +check("send_reply: chat_type сохранён", + model["inbox"]["m-901"]["chat_type"] == "supergroup" + and model["inbox"]["m-901"]["chat_title"] == "Кооп 42") +# реплай ушёл именно в чат-источник — проверено по sends выше; здесь — что +# ReplySent-событие тоже адресовано группе (to_chat) +_last = [e for e in es.get_events() + if e.get("event_type") == "ReplySent" and e.get("aggregate_id") == "m-901"] +check("send_reply: ReplySent-событие адресовано группе", + bool(_last) and str(GROUP_ID) in _last[-1]["data"], str(_last[-1:])) + +# ── 2. send_batch_reply: смешанный батч НЕ смешивает чаты ── +_recv(910, "группа: первое", GROUP_ID, "group", "Кооп 42", date=1780000010) +_recv(911, "группа: второе", GROUP_ID, "group", "Кооп 42", date=1780000011) +_recv(912, "личка: первое", PRIVATE_ID, "private", date=1780000012) + +sends.clear() +head = tg_inbox.send_batch_reply(["m-910", "m-911", "m-912"], "Сводный ответ") +sent_chats = sorted(c for c, _ in sends) +check("send_batch_reply: ДВЕ отправки (по числу чатов)", len(sends) == 2, str(sends)) +check("send_batch_reply: ответ в группу ушёл в группу", + sends and any(c == GROUP_ID for c, _ in sends), str(sends)) +check("send_batch_reply: ответ в личку ушёл в личку", + sends and any(c == PRIVATE_ID for c, _ in sends), str(sends)) +model = es.run_projections() +check("send_batch_reply: все 3 закрыты", + all(model["inbox"][f"m-91{i}"]["replied"] for i in (0, 1, 2))) + +# ── 3. _default_chat_id: дефолт — личка, не последняя группа ── +# последнее сообщение по ts — из группы (m-912 личка раньше m-911? нет: +# m-912 новее). Добавим ещё более новую группу, чтобы проверить приоритет. +_recv(920, "самое свежее — из группы", GROUP_ID, "group", "Кооп 42", date=1780000020) +default = tg_inbox._default_chat_id() +check("_default_chat_id: личка Alex, а не свежая группа", + default == PRIVATE_ID, str(default)) + +# старые записи без chat_type: группа = chat_id < 0 +es.append_event("inbox", "m-930", "MessageReceived", { + "update_id": 930, "chat_id": PRIVATE_ID, "text": "старая личка без типа", + "kind": "text", "ts": "2026-10-13T10:00:00+00:00"}) +es.append_event("inbox", "m-931", "MessageReceived", { + "update_id": 931, "chat_id": GROUP_ID, "text": "старая группа без типа", + "kind": "text", "ts": "2026-10-13T10:01:00+00:00"}) +default = tg_inbox._default_chat_id() +check("_default_chat_id: без chat_type — тоже личка (chat_id>0)", + default == PRIVATE_ID, str(default)) + +# только группы — фолбэк на группу (лучше, чем None) +print() +print("FAILS:", fails if fails else "нет") +sys.exit(1 if fails else 0)