kin/tests/test_kin_fix_025_regression.py
2026-03-21 11:59:22 +02:00

143 lines
8 KiB
Python
Raw Permalink 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.

"""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)
)