simbiot-harness/tests/test_msg_artifact_race.py

111 lines
5.6 KiB
Python
Raw 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 -*-
"""t-261009160906: анти-гонка уровней min/hour на входящих TG (hermetic).
Кейс 09.10: Alex попросил концепт (m-516174013). Мин-тик ответил и доставил
свой файл (16:01). Часовой тик, не видя ответа мин-уровня, сгенерировал СВОЙ
файл и доставил вторым (16:06) — Alex получил два разных файла на один запрос.
Dedup по artifact_id=file:<путь> не ловит: два разных файла = два разных ключа.
Фикс: связка «артефакт → входящее» (-m m-NNN у tg-deliver-file, msg= у
deliver_artifact, reply_artifact в read model). Если на входящее уже отвечено
иным артефактом — SKIP с событием. force обходит (ручная повторная доставка).
Запуск: ./venv/bin/python tests/test_msg_artifact_race.py
"""
import os
import subprocess
import sys
# ── изоляция: своя БД в /tmp (prod не трогаем) ──
_DST = "/tmp/simbiot-test-msg-artifact-race.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, commands
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)
es.init_db()
# ── подготовка: входящее m-990001, на которое мин-уровень уже ответил ──
es.append_event("inbox", "m-990001", "MessageReceived", {
"update_id": 990001, "chat_id": 42, "message_id": 1, "user_id": 1,
"text": "сделай концепт в html", "kind": "text", "date": 1791560000,
})
es.append_event("inbox", "m-990001", "ReplySent", {
"to_chat": 42, "text": "готово, отправил файлом", "tg_ok": True, "channel": "tg",
})
# файлы-двойники гонки: мин-уровень и часовой
_F1 = "/tmp/simbiot-test-race-min.html"
_F2 = "/tmp/simbiot-test-race-hour.html"
for f in (_F1, _F2):
with open(f, "w") as fh:
fh.write("<html><body>r</body></html>")
sends = []
# 1) мин-уровень доставил свой файл с привязкой к сообщению
ok1 = commands.deliver_artifact(f"file:{_F1}", kind="file", send=lambda: (sends.append(_F1) or True),
owner="test:min", caption="мин", msg="m-990001")
check("min-level file delivered", ok1 and sends == [_F1], sends)
model = es.run_projections()
m = model["inbox"]["m-990001"]
check("read model binds reply_artifact", m.get("reply_artifact") == f"file:{_F1}", m.get("reply_artifact"))
# 2) часовой уровень пытается доставить СВОЙ файл на то же входящее → SKIP
ok2 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
owner="test:hour", caption="час", msg="m-990001")
check("hour-level duplicate SKIPped", ok2 is False and sends == [_F1], sends)
db = es._conn()
skip = list(db.execute(
"SELECT data FROM events WHERE event_type='DeliverySkipped' AND data LIKE '%msg-already%'"))
check("DeliverySkipped event with reason", len(skip) == 1, len(skip))
# 3) force обходит защиту (ручная повторная доставка — явное решение человека)
ok3 = commands.deliver_artifact(f"file:{_F2}", kind="file", send=lambda: (sends.append(_F2) or True),
owner="test:hour", caption="час", force=True, msg="m-990001")
check("force bypasses guard", ok3 and sends == [_F1, _F2], sends)
# 4) доставки БЕЗ привязки к сообщению не задеваются (обратная совместимость)
ok4 = commands.deliver_artifact("file:/tmp/simbiot-test-race-unbound.html", kind="file",
send=lambda: (sends.append("unbound") or True),
owner="test", caption="x")
check("unrelated delivery unaffected", ok4 and sends[-1] == "unbound", sends)
# 5) второе входящее без артефакта: доставка с привязкой проходит свободно
es.append_event("inbox", "m-990002", "MessageReceived", {
"update_id": 990002, "chat_id": 42, "message_id": 2, "user_id": 1,
"text": "ещё один запрос", "kind": "text", "date": 1791560100,
})
es.append_event("inbox", "m-990002", "ReplySent", {
"to_chat": 42, "text": "ок", "tg_ok": True, "channel": "tg",
})
ok5 = commands.deliver_artifact("file:/tmp/simbiot-test-race-second.html", kind="file",
send=lambda: (sends.append("second") or True),
owner="test:hour", caption="y", msg="m-990002")
check("fresh msg accepts its artifact", ok5 and sends[-1] == "second", sends)
# 6) CLI: tg-deliver-file -m m-990001 с чужим артефактом → SKIP, выход 0
r = subprocess.run(
["./venv/bin/python", "cli.py", "tg-deliver-file", _F2, "-m", "m-990001",
"-c", "dup", "-k", "file:" + _F2],
capture_output=True, text=True, cwd="/root/simbiot-harness")
check("CLI SKIP on raced msg", r.returncode == 0 and "SKIP" in r.stdout,
r.stdout.strip()[:120] + r.stderr.strip()[-200:])
print("=" * 40)
print("FAILS:", len(fails), fails if fails else "— все проверки прошли")
sys.exit(1 if fails else 0)