111 lines
5.6 KiB
Python
111 lines
5.6 KiB
Python
# -*- 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)
|