"""Регресс 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__)))) 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)