- фото/документ/видео без подписи выпадали из триажа (_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>
85 lines
No EOL
3.8 KiB
Python
85 lines
No EOL
3.8 KiB
Python
"""Регресс e-261003004801: таймаут LLM-тика не должен валить зачистку.
|
||
|
||
Было: subprocess.run + TimeoutExpired без .pid → AttributeError в
|
||
изолированном runner'е ДО добивания процесс-группы. LLM-потомки воркера
|
||
выживали и писали в стор после смерти тика (race, burn роутера).
|
||
Теперь: Popen(start_new_session=True) → killpg(proc.pid); исключение —
|
||
честный TimeoutExpired, группа (с потомками) мертва до re-raise.
|
||
"""
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import time
|
||
import unittest
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
# изоляция стора (аудит 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 isolated_runner
|
||
|
||
|
||
class TimeoutKillTest(unittest.TestCase):
|
||
def setUp(self):
|
||
self.tmp = tempfile.mkdtemp(prefix="test_timeout_kill_")
|
||
self.worker = os.path.join(self.tmp, "fake_worker.py")
|
||
# фейковый воркер: спавнит потомка (имитация LLM-сабпроцесса) и спит
|
||
with open(self.worker, "w") as f:
|
||
f.write(
|
||
"import subprocess, time\n"
|
||
"child = subprocess.Popen(['sleep', '287'])\n"
|
||
"print(child.pid, flush=True)\n"
|
||
"time.sleep(300)\n"
|
||
)
|
||
self._orig = (isolated_runner._VENV_PY, isolated_runner._WORKER)
|
||
isolated_runner._VENV_PY = sys.executable
|
||
isolated_runner._WORKER = self.worker
|
||
|
||
def tearDown(self):
|
||
isolated_runner._VENV_PY, isolated_runner._WORKER = self._orig
|
||
|
||
def _cfg(self):
|
||
class Cfg:
|
||
name = "test"
|
||
model = "x"
|
||
effort = "low"
|
||
mandate = ""
|
||
can_shell = False
|
||
ctx_tokens = 1000
|
||
every = 300
|
||
return Cfg()
|
||
|
||
def test_timeout_kills_group_without_pid_crash(self):
|
||
with self.assertRaises(subprocess.TimeoutExpired):
|
||
isolated_runner.run_llm_tick(self._cfg(), "ping", timeout=3)
|
||
# группа воркера убита: потомок-«LLM» не выжил
|
||
time.sleep(0.5)
|
||
r = subprocess.run(["pgrep", "-c", "-f", "sleep 287"],
|
||
capture_output=True)
|
||
# pgrep: exit 1 = совпадений нет → потомок-«LLM» мёртв
|
||
self.assertEqual(r.returncode, 1,
|
||
f"LLM-потомок пережил таймаут добивания: {r.stdout!r}")
|
||
|
||
def test_normal_run_unaffected(self):
|
||
# короткий воркер: пишет результат в result_path как настоящий
|
||
worker = os.path.join(self.tmp, "ok_worker.py")
|
||
with open(worker, "w") as f:
|
||
f.write(
|
||
"import json, os, sys\n"
|
||
"payload = json.loads(sys.stdin.read())\n"
|
||
"json.dump({'ok': True, 'text': 'done',\n"
|
||
" 'usage': {}, 'trace_id': 't'}, open(payload['result_path'], 'w'))\n"
|
||
)
|
||
isolated_runner._WORKER = worker
|
||
text, usage, trace = isolated_runner.run_llm_tick(self._cfg(),
|
||
"ping", timeout=15)
|
||
self.assertEqual(text, "done")
|
||
self.assertEqual(trace, "t")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main(verbosity=2) |