👍 на отвеченные сообщения (правка Коди 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>
This commit is contained in:
Simbiot Harness 2026-10-18 10:55:02 +00:00
parent 09089902e9
commit 70a2015bae
3 changed files with 77 additions and 0 deletions

View file

@ -260,6 +260,9 @@ def looks_like_budget_stub(answer: str) -> bool:
return bool(_BUDGET_STUB_RE.search(s) and _BUDGET_STUB_MARKERS_RE.search(s)) return bool(_BUDGET_STUB_RE.search(s) and _BUDGET_STUB_MARKERS_RE.search(s))
MIN_ANSWER = 10 # символов в итоге задачи: короче — не результат («x», «готово»)
def complete_task_with_delivery(task_id: str, answer: str, level: str, def complete_task_with_delivery(task_id: str, answer: str, level: str,
result_ref: str = None): result_ref: str = None):
"""TaskCompleted + немедленная доставка результата в TG-чат. """TaskCompleted + немедленная доставка результата в TG-чат.
@ -279,6 +282,12 @@ def complete_task_with_delivery(task_id: str, answer: str, level: str,
raise ValueError(f"задача {task_id} в статусе {t['status']} — завершать можно только running") raise ValueError(f"задача {task_id} в статусе {t['status']} — завершать можно только running")
if (answer or "").strip() == "": if (answer or "").strip() == "":
raise ValueError("пустой результат — доставлять нечего, заверши как failed/requeue") raise ValueError("пустой результат — доставлять нечего, заверши как failed/requeue")
# мусорный итог (2026-10-18: задача закрыта с answer «x» — Alex получил в чат
# «✅ Готово … x»; раньше — «2», «готово»): результат должен говорить, что сделано
if len((answer or "").strip()) < MIN_ANSWER:
raise ValueError(
f"результат «{answer.strip()}» слишком короткий (< {MIN_ANSWER} симв.): опиши, что "
"сделано и где результат (файл/ссылка/вывод) — это уйдёт Alex в чат")
if looks_like_budget_stub(answer): if looks_like_budget_stub(answer):
requeue_task(task_id, reason="guard: 'бюджет исчерпан' — не результат, задача в ready (инцидент e-261001074527)") requeue_task(task_id, reason="guard: 'бюджет исчерпан' — не результат, задача в ready (инцидент e-261001074527)")
raise ValueError( raise ValueError(

View file

@ -541,9 +541,28 @@ def send_reply(msg_id: str, text: str, timeout: int = 35):
"to_chat": m["chat_id"], "text": text[:2000], "tg_ok": bool(r.get("ok")), "to_chat": m["chat_id"], "text": text[:2000], "tg_ok": bool(r.get("ok")),
"channel": "tg", "channel": "tg",
}) })
if r.get("ok"):
mark_answered(m)
return msg_id return msg_id
def mark_answered(m: dict):
"""✅ на входящее, на которое отвечено (t-261018103940): видно, какие
сообщения батча закрыты. Best effort: недоступность TG не мешает ответу
(ReplySent уже записан — повторно не ставим, т.к. replied-гейт выше).
Только TG-чаты; реакция ✅ заменяет маркер «взято» 👀."""
chat_id = m.get("chat_id")
if not chat_id or str(chat_id).startswith(("portal", "tui:")):
return
# ✅ не входит в допустимый Telegram набор реакций → 400 REACTION_EMPTY
# (Alex не видел галочку; проверено 2026-10-18). Валидный маркер — 👍.
tg_mid = m.get("message_id") or (m.get("update_id") or 0) % 100000
try:
set_reaction(chat_id, tg_mid, emoji="👍")
except Exception:
pass # реакция — только визуальный маркер, не блокирует ответ
def send_followup(msg_id: str, text: str, timeout: int = 35) -> bool: def send_followup(msg_id: str, text: str, timeout: int = 35) -> bool:
"""Досылка результата в ЧАТ входящего, на которое уже ответили (аудит 2026-10-16). """Досылка результата в ЧАТ входящего, на которое уже ответили (аудит 2026-10-16).
@ -688,6 +707,8 @@ def send_batch_reply(msg_ids: list, text: str, timeout: int = 35):
"tg_ok": bool(r.get("ok")), "tg_ok": bool(r.get("ok")),
"batch": {"id": batch_id, "size": len(pending), "head": head_mid}, "batch": {"id": batch_id, "size": len(pending), "head": head_mid},
}) })
if r.get("ok"):
mark_answered(m)
return head_mid return head_mid

View file

@ -72,5 +72,52 @@ check("правка принятого сообщения — не новое",
check("новое сообщение — новое", check("новое сообщение — новое",
not tg_inbox._already_received({"chat": {"id": G}, "message_id": 9999})) 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)") print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)