simbiot-harness/tests/test_tool_budget.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

130 lines
No EOL
4.8 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-260930031800: хард-кап тул-коллов в воркере (hermetic, без LLM).
Запуск: ./venv/bin/python tests/test_tool_budget.py
Проверяет: _wire_tool_budget отменяет вызовы после N реальных
исполнений (cancel_tool в BeforeToolCallEvent), останавливает цикл
(end_turn в AfterToolsEvent), счётчик растёт, лимит читается из env.
"""
import os
import sys
sys.path.insert(0, "/root/simbiot-harness")
# изоляция стора (аудит 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 _llm_worker
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
if not cond:
fails.append(name)
class _FakeBeforeEvent:
def __init__(self):
self.cancel_tool = False
class _FakeAfterEvent:
def __init__(self):
self.end_turn = False
class _FakeAgent:
def __init__(self):
self.hooks = []
def add_hook(self, fn, ev_type):
self.hooks.append((fn, ev_type))
def _budget_hooks(agent):
"""(before_hook, after_hook) из навешанных на фейковый агент."""
from strands.hooks import BeforeToolCallEvent, AfterToolsEvent
before = after = None
for fn, ev in agent.hooks:
if ev is BeforeToolCallEvent:
before = fn
elif ev is AfterToolsEvent:
after = fn
return before, after
# ── 1. хук отменяет вызовы после лимита исполнений ──
agent = _FakeAgent()
_llm_worker._wire_tool_budget(agent, max_calls=3)
before, after = _budget_hooks(agent)
check("budget: оба хука навешаны (before+after)", before is not None and after is not None)
# 6 вызовов: первые 3 исполняются, остальные отменены
cancels = 0
for _ in range(6):
ev = _FakeBeforeEvent()
before(ev)
if ev.cancel_tool:
cancels += 1
check("budget: 6 вызовов при лимите 3 → 3 отменены", cancels == 3, str(cancels))
# после партии: end_turn взводится, когда исполнений >= лимита
aev = _FakeAfterEvent()
after(aev)
check("budget: end_turn взведён после пробития лимита", bool(aev.end_turn), str(aev.end_turn))
check("budget: сообщение end_turn инструктивное",
"бюджет шагов" in str(aev.end_turn) and "оформи задачей" in str(aev.end_turn),
str(aev.end_turn)[:120])
# после halted: любые новые вызовы отменяются
ev = _FakeBeforeEvent()
before(ev)
check("budget: после halted все вызовы отменены", bool(ev.cancel_tool))
# ── 2. лимит не пробит → end_turn не взводится ──
agent2 = _FakeAgent()
_llm_worker._wire_tool_budget(agent2, max_calls=10)
before2, after2 = _budget_hooks(agent2)
for _ in range(5):
ev = _FakeBeforeEvent()
before2(ev)
check_ok = not ev.cancel_tool
aev2 = _FakeAfterEvent()
after2(aev2)
check("budget: 5 исполнений при лимите 10 → цикл живёт", aev2.end_turn is False,
str(aev2.end_turn))
# ── 3. лимит 0 — отменяет сразу ──
agent3 = _FakeAgent()
_llm_worker._wire_tool_budget(agent3, max_calls=0)
before3, _ = _budget_hooks(agent3)
ev = _FakeBeforeEvent()
before3(ev)
check("budget: max_calls=0 отменяет первый вызов", bool(ev.cancel_tool))
# ── 4. env-override читается воркером ──
os.environ["SIMBIOT_MAX_TOOL_CALLS"] = "7"
check("budget: env SIMBIOT_MAX_TOOL_CALLS читается",
int(os.environ.get("SIMBIOT_MAX_TOOL_CALLS") or 30) == 7)
del os.environ["SIMBIOT_MAX_TOOL_CALLS"]
# ── 5. реальные strands-хуки: create_harness + add_hook не падают ──
try:
from harness.runtime import setup_provider
setup_provider()
from strands_harness import create_harness
ag = create_harness(
model=None, instructions="test", session=None,
builtin_tools=[], memory=False, skills=False,
)
ag = _llm_worker._wire_tool_budget(ag, max_calls=30)
check("budget: хук навешан на реальный strands-агент", True)
except Exception as e:
check("budget: реальный агент с хуком не падает", False, str(e)[:200])
print()
print("FAILS:", len(fails))
sys.exit(1 if fails else 0)