- фото/документ/видео без подписи выпадали из триажа (_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>
151 lines
No EOL
8.6 KiB
Python
151 lines
No EOL
8.6 KiB
Python
# -*- 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) |