simbiot-harness/tests/test_tg_group_ux.py
Simbiot Harness 70a2015bae 👍 на отвеченные сообщения (правка Коди t-261018103940) + тест; мусорный итог задачи не принимается
Коди сам доработал tg_inbox (задача Alex 10:39): после ответа ставит
реакцию на каждое отвеченное сообщение (одиночный и сводный ответ),
нашёл, что ✅ Telegram не принимает (REACTION_EMPTY) — взял 👍. Правка
лежала незакоммиченной и без теста, а задачу он закрыл с итогом «x»
до того, как доделал (Alex пришло «✅ Готово … x»).
- коммит правки Коди как есть;
- тест (test_tg_group_ux +8): 👍 на сообщение, весь батч, портал без
  реакции, сбой реакции не ломает ответ;
- complete_task_with_delivery: итог короче 10 символов («x», «2»,
  «готово» — 3 из 338 закрытий с октября) отклоняется с подсказкой
  «опиши, что сделано и где результат».
Полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 10:55:02 +00:00

123 lines
6.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""TG в группах (аудит 2026-10-17, «реплаю Коди — не реагирует»). Hermetic.
Запуск: ./venv/bin/python tests/test_tg_group_ux.py
Проверяет: setMessageReaction шлёт reaction МАССИВОМ (было reaction_type
одиночным объектом → REACTION_EMPTY в группе, пустая реакция в личке);
ответ в группе — reply на сообщение (батч — на последнее), с
allow_sending_without_reply; в личке — без цитаты; досылка итога в группе —
тоже reply; правка уже принятого сообщения распознаётся как не новое.
"""
import json
import os
import sys
import tempfile
sys.path.insert(0, "/root/simbiot-harness")
os.environ["SIMBIOT_DB"] = os.path.join(tempfile.mkdtemp(prefix="simbiot_grpux_"), "harness.db")
from harness import es, tg_inbox
es.init_db()
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
if not cond:
fails.append(name)
API = []
tg_inbox._load_env = lambda: None
tg_inbox._api = lambda method, params, timeout=35: API.append((method, dict(params))) or {"ok": True, "result": True}
tg_inbox.set_reaction(-100, 7)
meth, prm = API[-1]
check("реакция: метод setMessageReaction", meth == "setMessageReaction")
check("реакция: параметр reaction — массив ReactionType",
json.loads(prm.get("reaction", "null")) == [{"type": "emoji", "emoji": "👀"}], prm)
check("реакция: нет мусорного reaction_type", "reaction_type" not in prm, prm)
G, P = -1004402779881, 7569223131
for uid, chat, ct, mid, ts in ((1, G, "supergroup", 2420, "2026-10-17T07:57:30"),
(2, G, "supergroup", 2428, "2026-10-17T07:57:31"),
(3, P, "private", 4950, "2026-10-17T07:57:32"),
(4, G, "supergroup", 2500, "2026-10-17T08:00:00")):
es.append_event("inbox", f"m-{uid}", "MessageReceived", {
"update_id": uid, "chat_id": chat, "chat_type": ct, "message_id": mid,
"text": "вопрос", "kind": "text", "first_name": "Alex"})
def sent():
return [p for m, p in API if m == "sendMessage"][-1]
API.clear()
tg_inbox.send_batch_reply(["m-1", "m-2"], "сводный ответ")
s = sent()
check("батч в группе: reply на последнее сообщение", s.get("reply_to_message_id") == 2428, s)
check("reply не теряется, если цель удалена", s.get("allow_sending_without_reply") == "true", s)
tg_inbox.send_reply("m-3", "ответ в личку")
check("личка: без цитаты", "reply_to_message_id" not in sent(), sent())
tg_inbox.send_reply("m-4", "ответ в группу")
check("одиночный ответ в группе: reply", sent().get("reply_to_message_id") == 2500, sent())
tg_inbox.send_followup("m-4", "итог работы")
check("досылка в группе: reply на исходный вопрос", sent().get("reply_to_message_id") == 2500
and sent().get("text") == "итог работы", sent())
check("правка принятого сообщения — не новое",
tg_inbox._already_received({"chat": {"id": G}, "message_id": 2428}))
check("новое сообщение — новое",
not tg_inbox._already_received({"chat": {"id": G}, "message_id": 9999}))
# ── 👍 на отвеченное (правка Коди, t-261018103940: ✅ Telegram не принимает) ──
def reactions():
return [p for m, p in API if m == "setMessageReaction"]
for uid, mid in ((10, 3001), (11, 3002), (12, 3003)):
es.append_event("inbox", f"m-{uid}", "MessageReceived", {
"update_id": uid, "chat_id": P, "chat_type": "private", "message_id": mid,
"text": "вопрос", "kind": "text", "first_name": "Alex"})
API.clear()
tg_inbox.send_reply("m-10", "ответ")
r = reactions()
check("ответ → 👍 на это сообщение", r and r[-1]["message_id"] == 3001
and json.loads(r[-1]["reaction"]) == [{"type": "emoji", "emoji": "👍"}], r)
API.clear()
tg_inbox.send_batch_reply(["m-11", "m-12"], "сводный")
check("батч → 👍 на каждое сообщение", sorted(p["message_id"] for p in reactions()) == [3002, 3003])
es.append_event("inbox", "m-13", "MessageReceived", {"update_id": 13, "chat_id": "portal:host",
"text": "x", "kind": "text"})
API.clear()
tg_inbox.mark_answered({"chat_id": "portal:host", "message_id": 1})
check("портал — без реакции", not reactions())
es.append_event("inbox", "m-14", "MessageReceived", {"update_id": 14, "chat_id": P,
"chat_type": "private", "message_id": 3004, "text": "вопрос", "kind": "text"})
_orig = tg_inbox.set_reaction
tg_inbox.set_reaction = lambda *a, **k: (_ for _ in ()).throw(RuntimeError("TG 400"))
try:
tg_inbox.send_reply("m-14", "ответ при упавшей реакции")
ok = es.run_projections()["inbox"]["m-14"].get("replied") is True
except Exception as e:
ok = False
check("сбой реакции не ломает ответ", ok)
tg_inbox.set_reaction = _orig
# ── мусорный итог задачи не принимается (2026-10-18: «x» ушёл Alex в чат) ──
from harness import commands
es.append_event("task", "t-x", "TaskSubmitted", {"id": "t-x", "title": "задача", "size": "S"})
es.append_event("task", "t-x", "TaskClaimed", {"level": "min"})
for junk in ("x", "готово", "2"):
try:
commands.complete_task_with_delivery("t-x", junk, level="min")
check(f"итог «{junk}» отклонён", False)
except ValueError:
check(f"итог «{junk}» отклонён", True)
tid, _ = commands.complete_task_with_delivery("t-x", "Реакция 👍 ставится после ответа", level="min")
check("нормальный итог принят", es.run_projections()["tasks"]["t-x"]["status"] == "done")
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0)