# -*- 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("r") 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)