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