# -*- coding: utf-8 -*- """e-260930062242/2510/2607/2741: гейт важности LLM-тика (hermetic, без LLM/БД). Запуск: ./venv/bin/python tests/test_gate.py Проверяет: collect_signals ранжирует события (critical/warn), gate_allows_llm решает по per-level порогу (SIMBIOT_GATE_), тихий тик не запускает LLM — гейт универсален для всех уровней. """ import os import sys sys.path.insert(0, "/root/simbiot-harness") # чистое окружение: env-капы гейта не должны влиять на тесты for _v in ("SIMBIOT_GATE_MIN", "SIMBIOT_GATE_HOUR", "SIMBIOT_GATE_DAY"): os.environ.pop(_v, None) # изоляция стора (аудит 2026-10-16): тест не пишет в боевой стор, даже # когда его гоняет тик харнеса под SIMBIOT_HOME (TTFTWatchdogRetry с # бюджетом 0.5с из этого набора оседали в проде) import tempfile as _iso_tf # noqa: E402 os.environ["SIMBIOT_DB"] = os.path.join(_iso_tf.mkdtemp(prefix="simbiot_test_"), "harness.db") from harness import scheduler from harness.levels import LEVELS fails = [] def check(name, cond, extra=""): print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "") if not cond: fails.append(name) def _model(tasks=None, inbox=None, escs=None, policy=None): return { "tasks": tasks or {}, "inbox": inbox or {}, "escalations": escs or {}, "policy": policy or {}, } # ── 1. Тишина: нет событий — гейт не пускает LLM ни на одном LLM-уровне ── empty = _model() for name in ("min", "hour", "day"): cfg = LEVELS[name] sigs = scheduler.collect_signals(cfg, empty) check(f"{name}: пустая модель → нет сигналов", sigs == [], str(sigs)) check(f"{name}: тихий тик → LLM не запускается", scheduler.gate_allows_llm(cfg, sigs) is False) # ── 2. Входящее TG без ответа — critical, минутный уровень просыпается ── inbox = {"m-1": {"update_id": 1, "text": "привет", "replied": False, "ignored": False}, "m-2": {"update_id": 2, "text": "/лестница", "replied": False, "ignored": False}} sigs = scheduler.collect_signals(LEVELS["min"], _model(inbox=inbox)) crit = [s for s in sigs if s[0] == "critical" and s[1] == "unreplied_inbox"] check("unreplied_inbox: 1 сигнал critical", len(crit) == 1, str(sigs)) check("unreplied_inbox: посчитаны только не-команды (1 из 2)", crit and "1 сообщений без ответа" in crit[0][2], crit[0][2] if crit else "") check("min: критичный сигнал → LLM запускается", scheduler.gate_allows_llm(LEVELS["min"], sigs) is True) # ── 3. Готовая задача доступного класса — critical ── tasks = {"t-1": {"id": "t-1", "status": "ready", "size": "M", "created": "2026-09-30T08:00:00"}} sigs = scheduler.collect_signals(LEVELS["hour"], _model(tasks=tasks)) check("hour: ready M-задача → critical", any( s[0] == "critical" and s[1] == "ready_task" for s in sigs), str(sigs)) # та же задача недоступна минутному уровню (класс M) — сигнала нет sigs_min = scheduler.collect_signals(LEVELS["min"], _model(tasks=tasks)) check("min: M-задача не его класса → сигнала ready_task нет", all(s[1] != "ready_task" for s in sigs_min), str(sigs_min)) # ── 4. Эскалации: адресованные УРОВНЮ будят его; чужие — нет (как в build_context) ── escs = {"e-1": {"id": "e-1", "status": "open", "severity": "critical", "to_level": "hour", "reason": "упал тик"}, "e-2": {"id": "e-2", "status": "open", "severity": "warn", "to_level": "hour", "reason": "warn-шум"}} # hour: critical-эскалация адресована ему → сигнал critical sigs_h = scheduler.collect_signals(LEVELS["hour"], _model(escs=escs)) check("hour: critical-эскалация → сигнал critical", any(s[0] == "critical" and s[1] == "escalation:e-1" for s in sigs_h), str(sigs_h)) check("hour: warn-эскалация достаточно (порог warn)", scheduler.gate_allows_llm(LEVELS["hour"], sigs_h) is True, str(sigs_h)) # min: те же эскалации адресованы hour — минутному они не видны, гейт закрыт sigs_min = scheduler.collect_signals(LEVELS["min"], _model(escs=escs)) check("min: эскалации hour-адреса не будят минуту (сигналов нет)", sigs_min == [], str(sigs_min)) check("min: только warn-эскалация → гейт закрыт (порог critical)", scheduler.gate_allows_llm(LEVELS["min"], sigs_min) is False) # эскалация, адресованная МИНУТЕ (если бы сек умел эскалировать) — будит минуту escs_min = {"e-6": {"id": "e-6", "status": "open", "severity": "critical", "to_level": "min", "reason": "критично для минуты"}} sigs = scheduler.collect_signals(LEVELS["min"], _model(escs=escs_min)) check("min: critical-эскалация, адресованная min, будит минуту", any(s[0] == "critical" and s[1] == "escalation:e-6" for s in sigs) and scheduler.gate_allows_llm(LEVELS["min"], sigs) is True, str(sigs)) # acked-эскалация не будит уровень escs_acked = {"e-3": {"id": "e-3", "status": "acked", "severity": "critical", "to_level": "hour", "reason": "закрыта"}} check("hour: acked-эскалация не создаёт сигналов", scheduler.collect_signals(LEVELS["hour"], _model(escs=escs_acked)) == []) # эскалация, адресованная ВЫШЕ уровня, не его зона (min не видит hour→day) escs_up = {"e-4": {"id": "e-4", "status": "open", "severity": "critical", "to_level": "day", "reason": "дневная тема"}} check("min: эскалация hour→day не адресована min", scheduler.collect_signals(LEVELS["min"], _model(escs=escs_up)) == []) check("day: эскалация hour→day видна day-уровню", any(s[1] == "escalation:e-4" for s in scheduler.collect_signals(LEVELS["day"], _model(escs=escs_up)))) # ── 5. Нагрузка: warn-сигнал, будит только уровни с порогом warn ── sig_load = scheduler.collect_signals(LEVELS["hour"], _model(policy={"cpu_alert_threshold": 8}), load=9.5) check("high_load: сигнал warn при load >= порога", any(s[0] == "warn" and s[1] == "high_load" for s in sig_load), str(sig_load)) check("min: high_load один не пробивает порог critical", scheduler.gate_allows_llm( LEVELS["min"], scheduler.collect_signals(LEVELS["min"], _model(policy={"cpu_alert_threshold": 8}), load=9.5) ) is False) # ── 6. Per-level порог из env: SIMBIOT_GATE_HOUR=critical / off ── try: os.environ["SIMBIOT_GATE_HOUR"] = "critical" sigs = scheduler.collect_signals(LEVELS["hour"], _model(escs={ "e-5": {"id": "e-5", "status": "open", "severity": "warn", "to_level": "hour", "reason": "warn"}})) check("env SIMBIOT_GATE_HOUR=critical: warn-эскалации мало", scheduler.gate_allows_llm(LEVELS["hour"], sigs) is False, str(sigs)) os.environ["SIMBIOT_GATE_HOUR"] = "off" check("env SIMBIOT_GATE_HOUR=off: LLM всегда", scheduler.gate_allows_llm(LEVELS["hour"], sigs) is True) finally: os.environ.pop("SIMBIOT_GATE_HOUR", None) # ── 7. Воркер: resolve_tool_budget per-level (e-260930060717/060904) ── from harness import _llm_worker check("tool budget: min=30 по умолчанию", _llm_worker.resolve_tool_budget("min") == 30) check("tool budget: hour=300 по умолчанию (просьба Alex «хоть 300»)", _llm_worker.resolve_tool_budget("hour") == 300) os.environ["SIMBIOT_MAX_TOOL_CALLS_HOUR"] = "100" try: check("tool budget: env SIMBIOT_MAX_TOOL_CALLS_HOUR=100 бьёт дефолт", _llm_worker.resolve_tool_budget("hour") == 100) finally: os.environ.pop("SIMBIOT_MAX_TOOL_CALLS_HOUR", None) print() print("FAILED: " + str(len(fails)) if fails else "ALL PASS") sys.exit(1 if fails else 0)