t-261013113629: маршрутизация ответов TG в чат-источник — chat_type/chat_title в MessageReceived, дефолтный чат уведомлений = личка Alex (chat_id>0 фолбэк для старых записей); тест 11 PASS (группа→группа, личка→личка, смешанный батч, дефолт)

This commit is contained in:
Simbiot Harness 2026-10-13 11:44:43 +00:00
parent 5a889e42a4
commit 5b1d30a0ff
2 changed files with 131 additions and 1 deletions

View file

@ -264,6 +264,11 @@ def receive_message(update: dict):
data = { data = {
"update_id": update["update_id"], "update_id": update["update_id"],
"chat_id": chat.get("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): # tg message_id нужен для setMessageReaction (реакция «seen», e-261002202857):
# update_id ≠ message_id (глобальный стрим vs чат), фолбэк — старый hash # update_id ≠ message_id (глобальный стрим vs чат), фолбэк — старый hash
"message_id": msg.get("message_id"), "message_id": msg.get("message_id"),
@ -547,7 +552,20 @@ def _default_chat_id():
and not m.get("ignored")] and not m.get("ignored")]
if not inbox: if not inbox:
return None # некому: чат ещё не известен 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, def _post_multipart(method: str, fields: dict, file_path: str,

112
tests/test_reply_routing.py Normal file
View file

@ -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)