simbiot-harness/tests/test_gate.py
Simbiot Harness 871a3b15a3 feat(gate): гейт важности LLM-тика по per-level порогу + model в событиях
- гейт (e-260930062242/2510/2607/2741): collect_signals ранжирует события
  (unreplied_inbox/ready_task/critical-эскалация = critical; warn-эскалации,
  high_load = warn), gate_allows_llm решает по порогу уровня
  (SIMBIOT_GATE_<LEVEL>, дефолты min=critical/hour=warn/day=warn);
  тихий такт — GateSkipped + дайджест-заглушка, без LLM и без расхода роутера
- model в BudgetSpent/InvocationStarted (e-260930062120): видно, какая модель
  и уровень тратили кредиты (strands не выносит фактическую модель в usage —
  пишем запрошенный model_id)
- per-level лимиты тул-колов уже в дереве: resolve_tool_budget (min=30/hour=300,
  env-оверрайд) (e-260930060717/060904)
- tests/test_gate.py: 28 hermetic-проверок, ALL PASS
- simbiot-presentation.html: презентация Simbiot (t-260930053538), 10 слайдов
2026-09-30 08:29:54 +00:00

145 lines
No EOL
8.2 KiB
Python
Raw 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)
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)