simbiot-harness/tests/test_gate.py
Simbiot Harness b257c32558 аудит @juno42bot 2026-10-16: медиа без подписи в триаже, мин-тик по входящему, тишина сек-уровня, изоляция тестов
- фото/документ/видео без подписи выпадали из триажа (_pending_inbox и
  build_context пускали только текст/голос): висели до 23 ч, все 4
  неотвеченных за неделю — такие. Один фильтр _is_pending, медиа с тегом
  и путём файла; окно свежести SIMBIOT_MEDIA_PENDING_H=6 — старые не
  будят LLM задним числом; стикер без текста — не повод.
- MinNudge: мин-тик через 3 с тишины после MessageReceived/Transcribed, не
  ждём границы минуты (было: медиана 36 с до старта LLM, p90 93 с при
  24 с самой модели). Слот следующего такта, граница его пропускает;
  SIMBIOT_MIN_NUDGE=0 — выключить.
- сек-уровень: CycleTicked+ReflexObserved каждую секунду = 1.35 млн
  подписанных событий/нед (98% стора 1.37 ГБ). Теперь heartbeat раз в
  60 с + такты с событием (алерт, команды, каналы); «reflex ok» не
  спамит журнал.
- 6 тестов писали в боевой стор под SIMBIOT_HOME тика (TTFTWatchdogRetry
  с бюджетом 0.5 с в проде — это они): изоляция SIMBIOT_DB.
Тест: tests/test_tick_latency_noise.py 20 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:32:15 +00:00

151 lines
No EOL
8.6 KiB
Python
Raw Permalink 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 -*-
"""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_<LEVEL>),
тихий тик не запускает 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)