# -*- coding: utf-8 -*- """Аудит 2026-10-16: медиа без подписи в триаже, тик по входящему, тишина сек-уровня. Запуск: ./venv/bin/python tests/test_tick_latency_noise.py (hermetic, свой стор) Проверяет: фото/документ/видео без подписи — ожидают ответа (свежие ≤ 6 ч), старые — нет; стикер без текста — нет; команда — нет; контекст тика показывает тег медиа с путём файла; MinNudge срабатывает после debounce на MessageReceived/MessageTranscribed, игнорирует прочие события, выключается env; сек-уровень пишет CycleTicked/ReflexObserved только heartbeat'ом и в тактах с событием (команды/алерт), а тихие такты — без записи в стор. """ import json import os import sys import tempfile import time from datetime import datetime, timedelta, timezone sys.path.insert(0, "/root/simbiot-harness") os.environ["SIMBIOT_DB"] = os.path.join(tempfile.mkdtemp(prefix="simbiot_latnoise_"), "harness.db") for _v in ("SIMBIOT_MIN_NUDGE", "SIMBIOT_MIN_NUDGE_DEBOUNCE", "SIMBIOT_MEDIA_PENDING_H", "SIMBIOT_SEC_HEARTBEAT", "SIMBIOT_DECISION"): os.environ.pop(_v, None) from harness import es, scheduler from harness.levels import LEVELS 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) now = datetime.now(timezone.utc) def msg(uid, kind="text", text="", age_h=0.0, **kw): return {"update_id": uid, "kind": kind, "text": text, "replied": False, "ignored": False, "ts": (now - timedelta(hours=age_h)).isoformat(), "chat_id": 1, "first_name": "Alex", **kw} # ── 1. медиа без подписи в триаже ── inbox = {f"m-{m['update_id']}": m for m in [ msg(1, "photo", age_h=0.1, file_path="inbox/1.jpg"), msg(2, "document", age_h=1, file_name="dogovor.pdf", file_path="inbox/dogovor.pdf"), msg(3, "video", age_h=0.2), msg(4, "photo", age_h=30), # старое, выпало до фикса msg(5, "sticker", age_h=0.1), msg(6, "text", "/статус", age_h=0.1), msg(7, "text", "привет", age_h=50), # текст — как раньше, без окна свежести msg(8, "voice", age_h=40), # голосовое — как раньше msg(9, "photo", age_h=0.1, replied=True), ]} ids = {m["update_id"] for m in scheduler._pending_inbox({"inbox": inbox})} check("фото/документ/видео без подписи — ждут ответа", {1, 2, 3} <= ids, ids) check("старое медиа (>6ч) — не будит LLM", 4 not in ids, ids) check("стикер без текста — не ждёт", 5 not in ids, ids) check("команда — не для LLM", 6 not in ids, ids) check("текст и голосовое — как раньше", {7, 8} <= ids, ids) check("отвеченное — нет", 9 not in ids, ids) os.environ["SIMBIOT_MEDIA_PENDING_H"] = "48" check("окно свежести настраивается env", 4 in {m["update_id"] for m in scheduler._pending_inbox({"inbox": inbox})}) os.environ.pop("SIMBIOT_MEDIA_PENDING_H") sig = scheduler.collect_signals(LEVELS["min"], {"inbox": {"m-1": inbox["m-1"]}, "tasks": {}}) check("фото без подписи → critical-сигнал мин-уровня", any(s[1] == "unreplied_inbox" for s in sig), sig) ctx = scheduler.build_context(LEVELS["min"], {"inbox": {"m-2": inbox["m-2"]}, "tasks": {}, "escalations": {}, "policy": {}}, batch=[]) check("контекст: тег документа с путём и «без подписи»", "[document, файл: inbox/dogovor.pdf, без подписи]" in ctx, ctx[:400]) # ── 2. MinNudge ── os.environ["SIMBIOT_MIN_NUDGE_DEBOUNCE"] = "0.3" n = scheduler.MinNudge() n.poll() # первый проход — только запоминает хвост check("nudge: на старте не срабатывает", not n.due()) es.append_event("cycle", "noise:1", "DigestWritten", {"level": "min"}) n.poll() check("nudge: прочие события не будят", n.pending_since is None) es.append_event("tg", "m-777", "MessageReceived", {"update_id": 777, "text": "эй"}) n.poll() check("nudge: входящее замечено, но debounce", n.pending_since is not None and not n.due()) time.sleep(0.35) check("nudge: после debounce — пора", n.due()) n.fired() check("nudge: fired сбрасывает", not n.due()) es.append_event("tg", "m-777", "MessageTranscribed", {"text": "расшифровка"}) n.poll(); time.sleep(0.35) check("nudge: расшифровка голосового тоже будит", n.due()) os.environ["SIMBIOT_MIN_NUDGE"] = "0" n2 = scheduler.MinNudge(); n2.poll() es.append_event("tg", "m-778", "MessageReceived", {"update_id": 778, "text": "эй"}) n2.poll(); time.sleep(0.35) check("nudge: SIMBIOT_MIN_NUDGE=0 выключает", not n2.due()) os.environ.pop("SIMBIOT_MIN_NUDGE") # ── 3. сек-уровень: heartbeat + такты с событием ── def count(et): return sum(1 for e in es.get_events(0, 100000) if e["event_type"] == et) sec = LEVELS["sec"] scheduler._SEC_HB.update(last=None, seq=None) base_t, base_r = count("CycleTicked"), count("ReflexObserved") start = 1_000_000 for i in range(130): scheduler.process_level(sec, {"inbox": {}, "tasks": {}, "policy": {"cpu_alert_threshold": 1e9}}, seq=start + i) dt, dr = count("CycleTicked") - base_t, count("ReflexObserved") - base_r check("сек: 130 тихих тактов → 3 heartbeat CycleTicked (0, 60, 120)", dt == 3, dt) check("сек: ReflexObserved только heartbeat", dr == 3, dr) scheduler.process_level(sec, {"inbox": {}, "tasks": {}, "policy": {"cpu_alert_threshold": 0}}, seq=start + 131) check("сек: алерт нагрузки пишется вне heartbeat", count("ReflexObserved") - base_r == 4) last = [json.loads(e["data"]) for e in es.get_events(0, 100000) if e["event_type"] == "ReflexObserved"][-1] check("сек: в записи алерта есть alert", "alert" in last, last) print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)") sys.exit(1 if fails else 0)