kin/tests/test_kin_fix_025_regression.py

144 lines
8 KiB
Python
Raw Normal View History

2026-03-21 11:59:22 +02:00
"""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)
)