kin/tests/test_kin_docs_008_regression.py
2026-03-19 21:25:43 +02:00

341 lines
18 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-DOCS-008 — Добавить паттерн error_coordinator для крупных баг-расследований.
Acceptance criteria:
1. agents/prompts/error_coordinator.md существует и содержит все 5 стандартных секций (decision #940)
2. output_schema в specialists.yaml содержит обязательные поля:
fault_groups, primary_faults, streams (и полный набор из 5 полей) (decision #952, #957)
3. Параметризованный тест — каждое из обязательных полей output_schema присутствует (decision #957)
4. error_coordinator зарегистрирован в specialists.yaml с корректными атрибутами (decision #954)
5. Route template 'multi_bug_debug' существует и содержит шаги [error_coordinator, debugger, tester]
6. pm.md содержит правило активации error_coordinator при ≥2 взаимосвязанных ошибках
"""
from pathlib import Path
import pytest
import yaml
SPECIALISTS_YAML = Path(__file__).parent.parent / "agents" / "specialists.yaml"
PROMPTS_DIR = Path(__file__).parent.parent / "agents" / "prompts"
ERROR_COORDINATOR_PROMPT = PROMPTS_DIR / "error_coordinator.md"
PM_PROMPT = PROMPTS_DIR / "pm.md"
REQUIRED_SECTIONS = [
"## Working Mode",
"## Focus On",
"## Quality Checks",
"## Return Format",
"## Constraints",
]
# Обязательные поля output_schema (decision #952, #957)
ERROR_COORDINATOR_REQUIRED_SCHEMA_FIELDS = {
"status",
"fault_groups",
"primary_faults",
"cascading_symptoms",
"streams",
"reintegration_checklist",
}
def _load_yaml():
return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8"))
# ===========================================================================
# 1. Структура промпта — 5 стандартных секций (AC-1, decision #940)
# ===========================================================================
class TestErrorCoordinatorPromptStructure:
"""agents/prompts/error_coordinator.md существует и содержит все 5 стандартных секций."""
def test_prompt_file_exists(self):
"""Файл agents/prompts/error_coordinator.md существует."""
assert ERROR_COORDINATOR_PROMPT.exists(), (
f"Промпт error_coordinator не найден: {ERROR_COORDINATOR_PROMPT}"
)
def test_prompt_file_is_not_empty(self):
"""Файл error_coordinator.md не пустой (более 100 символов)."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert len(content.strip()) > 100
@pytest.mark.parametrize("section", REQUIRED_SECTIONS)
def test_prompt_has_required_section(self, section):
"""Промпт error_coordinator.md содержит каждую из 5 стандартных секций."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert section in content, (
f"error_coordinator.md не содержит обязательную секцию {section!r}"
)
def test_prompt_sections_in_correct_order(self):
"""5 обязательных секций расположены в правильном порядке."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
positions = [content.find(sec) for sec in REQUIRED_SECTIONS]
assert all(p != -1 for p in positions), "Не все 5 секций найдены в error_coordinator.md"
assert positions == sorted(positions), (
f"Секции в error_coordinator.md расположены не по порядку. "
f"Позиции: {dict(zip(REQUIRED_SECTIONS, positions))}"
)
def test_prompt_has_input_section(self):
"""Промпт error_coordinator.md содержит секцию ## Input."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "## Input" in content, "error_coordinator.md не содержит секцию '## Input'"
def test_prompt_contains_blocked_protocol(self):
"""Промпт error_coordinator.md содержит Blocked Protocol с полем blocked_reason."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "blocked_reason" in content, (
"error_coordinator.md не содержит 'blocked_reason' — Blocked Protocol обязателен"
)
def test_prompt_contains_blocked_at(self):
"""Промпт error_coordinator.md содержит поле blocked_at в Blocked Protocol."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "blocked_at" in content, (
"error_coordinator.md не содержит 'blocked_at' в Blocked Protocol"
)
# ===========================================================================
# 2. output_schema — обязательные поля (AC-2, decision #952)
# ===========================================================================
class TestErrorCoordinatorOutputSchemaFields:
"""output_schema error_coordinator содержит все обязательные поля (decision #952)."""
def test_specialist_has_output_schema(self):
"""error_coordinator имеет поле output_schema в specialists.yaml."""
data = _load_yaml()
role = data["specialists"]["error_coordinator"]
assert "output_schema" in role, "error_coordinator должен иметь output_schema"
def test_output_schema_has_fault_groups(self):
"""output_schema содержит ключевое поле fault_groups."""
data = _load_yaml()
schema = data["specialists"]["error_coordinator"]["output_schema"]
assert "fault_groups" in schema, (
"output_schema error_coordinator обязана содержать поле 'fault_groups'"
)
def test_output_schema_has_primary_faults(self):
"""output_schema содержит ключевое поле primary_faults."""
data = _load_yaml()
schema = data["specialists"]["error_coordinator"]["output_schema"]
assert "primary_faults" in schema, (
"output_schema error_coordinator обязана содержать поле 'primary_faults'"
)
def test_output_schema_has_streams(self):
"""output_schema содержит ключевое поле streams."""
data = _load_yaml()
schema = data["specialists"]["error_coordinator"]["output_schema"]
assert "streams" in schema, (
"output_schema error_coordinator обязана содержать поле 'streams'"
)
# ===========================================================================
# 3. Параметризованный тест отсутствующих полей output_schema (AC-3, decision #957)
# ===========================================================================
class TestErrorCoordinatorOutputSchemaParametrized:
"""Параметризованный тест: каждое из обязательных полей output_schema присутствует (decision #957)."""
@pytest.mark.parametrize("required_field", sorted(ERROR_COORDINATOR_REQUIRED_SCHEMA_FIELDS))
def test_output_schema_contains_required_field(self, required_field):
"""output_schema error_coordinator содержит обязательное поле."""
data = _load_yaml()
schema = data["specialists"]["error_coordinator"]["output_schema"]
assert required_field in schema, (
f"output_schema error_coordinator обязана содержать поле {required_field!r}"
)
# ===========================================================================
# 4. Регистрация специалиста в specialists.yaml (AC-4, decision #954)
# ===========================================================================
class TestErrorCoordinatorSpecialistsEntry:
"""error_coordinator зарегистрирован в specialists.yaml с корректными атрибутами (decision #954)."""
def test_error_coordinator_exists_in_specialists(self):
"""error_coordinator присутствует в секции specialists."""
data = _load_yaml()
assert "error_coordinator" in data.get("specialists", {}), (
"error_coordinator отсутствует в specialists.yaml"
)
def test_error_coordinator_model_is_sonnet(self):
"""error_coordinator использует модель sonnet."""
data = _load_yaml()
role = data["specialists"]["error_coordinator"]
assert role.get("model") == "sonnet", (
f"Ожидался model=sonnet, получили: {role.get('model')}"
)
def test_error_coordinator_permissions_is_read_only(self):
"""error_coordinator имеет permissions=read_only (анализ без изменений кода)."""
data = _load_yaml()
role = data["specialists"]["error_coordinator"]
assert role.get("permissions") == "read_only", (
f"Ожидался permissions=read_only, получили: {role.get('permissions')}"
)
def test_error_coordinator_tools_include_read_grep_glob(self):
"""error_coordinator имеет инструменты Read, Grep, Glob."""
data = _load_yaml()
tools = data["specialists"]["error_coordinator"].get("tools", [])
for tool in ("Read", "Grep", "Glob"):
assert tool in tools, f"error_coordinator должен иметь инструмент {tool!r}"
def test_error_coordinator_not_in_any_department_workers(self):
"""error_coordinator не входит ни в один department workers — вставляется через PM routing."""
data = _load_yaml()
for dept_name, dept in data.get("departments", {}).items():
workers = dept.get("workers", [])
assert "error_coordinator" not in workers, (
f"error_coordinator не должен быть в workers департамента '{dept_name}'. "
"Специалист вставляется через PM routing rule, не через department."
)
# ===========================================================================
# 5. Route template 'multi_bug_debug' (AC-5)
# ===========================================================================
class TestMultiBugDebugRoute:
"""Route template 'multi_bug_debug' существует и содержит правильные шаги."""
def test_multi_bug_debug_route_exists(self):
"""Route template 'multi_bug_debug' присутствует в specialists.yaml."""
data = _load_yaml()
routes = data.get("routes", {})
assert "multi_bug_debug" in routes, (
"Route template 'multi_bug_debug' отсутствует в specialists.yaml"
)
def test_multi_bug_debug_first_step_is_error_coordinator(self):
"""Route 'multi_bug_debug': первый шаг — error_coordinator."""
data = _load_yaml()
steps = data["routes"]["multi_bug_debug"]["steps"]
assert steps[0] == "error_coordinator", (
f"Первый шаг 'multi_bug_debug' должен быть 'error_coordinator', получили: {steps[0]!r}"
)
def test_multi_bug_debug_contains_debugger(self):
"""Route 'multi_bug_debug' содержит шаг 'debugger'."""
data = _load_yaml()
steps = data["routes"]["multi_bug_debug"]["steps"]
assert "debugger" in steps, (
f"Route 'multi_bug_debug' должен содержать 'debugger'. Шаги: {steps}"
)
def test_multi_bug_debug_contains_tester(self):
"""Route 'multi_bug_debug' содержит шаг 'tester'."""
data = _load_yaml()
steps = data["routes"]["multi_bug_debug"]["steps"]
assert "tester" in steps, (
f"Route 'multi_bug_debug' должен содержать 'tester'. Шаги: {steps}"
)
def test_multi_bug_debug_steps_exact(self):
"""Route 'multi_bug_debug' содержит ровно шаги [error_coordinator, debugger, tester]."""
data = _load_yaml()
steps = data["routes"]["multi_bug_debug"]["steps"]
assert steps == ["error_coordinator", "debugger", "tester"], (
f"Ожидались шаги ['error_coordinator', 'debugger', 'tester'], получили: {steps}"
)
def test_multi_bug_debug_has_description(self):
"""Route 'multi_bug_debug' имеет поле description."""
data = _load_yaml()
route = data["routes"]["multi_bug_debug"]
assert "description" in route and route["description"], (
"Route 'multi_bug_debug' должен иметь непустое поле 'description'"
)
# ===========================================================================
# 6. pm.md — правило активации error_coordinator (AC-6)
# ===========================================================================
class TestPmMultiBugRoutingRule:
"""pm.md содержит правило активации error_coordinator при ≥2 взаимосвязанных ошибках."""
def test_pm_mentions_error_coordinator(self):
"""pm.md упоминает 'error_coordinator' как специалиста для multi-bug сценария."""
content = PM_PROMPT.read_text(encoding="utf-8")
assert "error_coordinator" in content, (
"pm.md должен упоминать 'error_coordinator' в правилах маршрутизации"
)
def test_pm_mentions_multi_bug_debug_route(self):
"""pm.md упоминает route template 'multi_bug_debug'."""
content = PM_PROMPT.read_text(encoding="utf-8")
assert "multi_bug_debug" in content, (
"pm.md должен упоминать route template 'multi_bug_debug'"
)
def test_pm_has_activation_threshold_two_bugs(self):
"""pm.md содержит правило активации при ≥2 взаимосвязанных ошибках."""
content = PM_PROMPT.read_text(encoding="utf-8")
# Проверяем, что присутствует хотя бы одно упоминание порогового правила ≥2
has_threshold = "≥2" in content or ">= 2" in content or "2 взаимосвяз" in content
assert has_threshold, (
"pm.md должен содержать правило активации error_coordinator при ≥2 связанных ошибках"
)
def test_pm_quality_check_mentions_error_coordinator_first(self):
"""pm.md содержит Quality Check: при ≥2 багах pipeline[0].role == error_coordinator."""
content = PM_PROMPT.read_text(encoding="utf-8")
assert "pipeline[0].role == error_coordinator" in content, (
"pm.md Quality Checks должен проверять pipeline[0].role == error_coordinator "
"при задаче с ≥2 взаимосвязанными багами"
)
# ===========================================================================
# 7. Промпт определяет ключевые поля output_schema в тексте
# ===========================================================================
class TestErrorCoordinatorPromptOutputFields:
"""Промпт error_coordinator.md определяет ключевые поля выходной схемы в ## Return Format."""
def test_prompt_defines_fault_groups(self):
"""Промпт определяет поле 'fault_groups' в Return Format."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "fault_groups" in content, (
"error_coordinator.md должен определять поле 'fault_groups'"
)
def test_prompt_defines_primary_faults(self):
"""Промпт определяет поле 'primary_faults' в Return Format."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "primary_faults" in content, (
"error_coordinator.md должен определять поле 'primary_faults'"
)
def test_prompt_defines_streams(self):
"""Промпт определяет поле 'streams' в Return Format."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "streams" in content, (
"error_coordinator.md должен определять поле 'streams'"
)
def test_prompt_defines_reintegration_checklist(self):
"""Промпт определяет поле 'reintegration_checklist' в Return Format."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "reintegration_checklist" in content, (
"error_coordinator.md должен определять поле 'reintegration_checklist'"
)
def test_prompt_defines_cascading_symptoms(self):
"""Промпт определяет поле 'cascading_symptoms' в Return Format."""
content = ERROR_COORDINATOR_PROMPT.read_text(encoding="utf-8")
assert "cascading_symptoms" in content, (
"error_coordinator.md должен определять поле 'cascading_symptoms'"
)