"""Regression tests for KIN-FIX-025 — запрет time.sleep в pipeline execution thread. Корень бага KIN-145: blocking sleep в pipeline-треде останавливает весь runner-процесс. При краше родителя через _check_parent_alive роняет все in-flight pipeline. Правило: - agents/ — time.sleep ЗАПРЕЩЁН без исключений - core/ — time.sleep разрешён только в явно задокументированных файлах (ALLOWLIST) ALLOWLIST (core-файлы с законным time.sleep): - core/watchdog.py — daemon-поток, работает независимо от pipeline, sleep-цикл штатный - core/worktree.py — retry-delay, вызывается только с explicit max_retries kwargs, runner.py НЕ передаёт эти kwargs (guard: test_KIN-145_regression.py) Тест сканирует все .py файлы в core/ и agents/, исключая test_*.py и *.pyc. Падает при нахождении time.sleep вне allowlist, выводя путь и строку нарушения. """ import re from pathlib import Path import pytest # ───────────────────────────────────────────────────────────────────────────── # Конфигурация # ───────────────────────────────────────────────────────────────────────────── PROJECT_ROOT = Path(__file__).parent.parent # Файлы core/, в которых time.sleep допустим (с задокументированной причиной) CORE_SLEEP_ALLOWLIST: set[str] = { "core/watchdog.py", # daemon-поток: sleep-цикл мониторинга, не pipeline "core/worktree.py", # retry-delay: вызывается с explicit kwargs, НЕ из runner.py } SLEEP_PATTERN = re.compile(r"\btime\.sleep\s*\(") def _find_sleep_violations(directory: Path, allowlist: set[str]) -> list[str]: """Ищет time.sleep в .py файлах директории, возвращает список нарушений.""" violations = [] for py_file in sorted(directory.rglob("*.py")): # Исключаем тест-файлы if py_file.name.startswith("test_"): continue # Нормализуем путь относительно проекта для сравнения с allowlist rel_path = py_file.relative_to(PROJECT_ROOT) rel_str = str(rel_path).replace("\\", "/") if rel_str in allowlist: continue # Сканируем строки файла try: lines = py_file.read_text(encoding="utf-8").splitlines() except (OSError, UnicodeDecodeError): continue for lineno, line in enumerate(lines, start=1): if SLEEP_PATTERN.search(line): violations.append(f"{rel_str}:{lineno}: {line.strip()}") return violations # ───────────────────────────────────────────────────────────────────────────── # Тест 1: agents/ — time.sleep запрещён полностью # ───────────────────────────────────────────────────────────────────────────── class TestNoSleepInAgents: def test_no_time_sleep_in_agents(self): """KIN-FIX-025: agents/ не должен содержать time.sleep. Blocking sleep в pipeline-треде → каскадное падение через _check_parent_alive. Допустимые альтернативы: asyncio.sleep, threading.Event.wait, отдельный поток. """ agents_dir = PROJECT_ROOT / "agents" violations = _find_sleep_violations(agents_dir, allowlist=set()) assert not violations, ( "НАРУШЕНИЕ KIN-FIX-025: time.sleep найден в agents/ (pipeline thread).\n" "Blocking sleep в pipeline-треде → каскадное падение всех in-flight задач.\n" "Используй asyncio.sleep / threading.Event.wait / отдельный поток.\n" "Нарушения:\n" + "\n".join(f" {v}" for v in violations) ) # ───────────────────────────────────────────────────────────────────────────── # Тест 2: core/ — time.sleep запрещён вне allowlist # ───────────────────────────────────────────────────────────────────────────── class TestNoSleepInCoreOutsideAllowlist: def test_no_time_sleep_in_core_outside_allowlist(self): """KIN-FIX-025: core/ не должен содержать time.sleep вне CORE_SLEEP_ALLOWLIST. Разрешённые исключения (CORE_SLEEP_ALLOWLIST): - core/watchdog.py — daemon-поток, не pipeline - core/worktree.py — retry-delay с explicit kwargs Любой новый time.sleep в core/ требует явного добавления в allowlist с документированным обоснованием. """ core_dir = PROJECT_ROOT / "core" violations = _find_sleep_violations(core_dir, allowlist=CORE_SLEEP_ALLOWLIST) assert not violations, ( "НАРУШЕНИЕ KIN-FIX-025: time.sleep найден в core/ вне CORE_SLEEP_ALLOWLIST.\n" "Если sleep необходим — добавь файл в CORE_SLEEP_ALLOWLIST в этом тест-файле\n" "с задокументированным обоснованием (daemon-поток или retry с explicit kwargs).\n" "Нарушения:\n" + "\n".join(f" {v}" for v in violations) ) def test_allowlisted_files_still_exist(self): """KIN-FIX-025: файлы из CORE_SLEEP_ALLOWLIST должны существовать. Если файл удалён — убери его из allowlist, чтобы allowlist не гнил. """ missing = [] for rel_path in CORE_SLEEP_ALLOWLIST: full_path = PROJECT_ROOT / rel_path if not full_path.exists(): missing.append(rel_path) assert not missing, ( "Файлы из CORE_SLEEP_ALLOWLIST не найдены — удали их из allowlist:\n" + "\n".join(f" {p}" for p in missing) ) def test_allowlisted_files_actually_contain_sleep(self): """KIN-FIX-025: файлы из CORE_SLEEP_ALLOWLIST должны реально содержать time.sleep. Если sleep убрали — убери файл из allowlist, чтобы allowlist не стал dead weight. """ stale = [] for rel_path in CORE_SLEEP_ALLOWLIST: full_path = PROJECT_ROOT / rel_path if not full_path.exists(): continue # покрывается тестом выше content = full_path.read_text(encoding="utf-8") if not SLEEP_PATTERN.search(content): stale.append(rel_path) assert not stale, ( "Файлы из CORE_SLEEP_ALLOWLIST не содержат time.sleep — удали их из allowlist:\n" + "\n".join(f" {p}" for p in stale) )