"""Regression tests for KIN-DOCS-007 — cto_advisor: strategic technical reviewer. Acceptance criteria: 1. cto_advisor зарегистрирован в specialists.yaml с model=opus 2. cto_advisor имеет permissions=read_only 3. output_schema содержит поля: status, scalability_assessment, strategic_risks, strategic_verdict, recommendation, notes 4. agents/prompts/cto_advisor.md существует и содержит все 5 стандартных секций 5. Промпт содержит поля выходной схемы: scalability_assessment, strategic_verdict 6. Промпт содержит Blocked Protocol (blocked_at) 7. cto_advisor НЕ входит ни в один department workers (опциональный специалист) """ 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" CTO_ADVISOR_PROMPT = PROMPTS_DIR / "cto_advisor.md" REQUIRED_SECTIONS = [ "## Working Mode", "## Focus On", "## Quality Checks", "## Return Format", "## Constraints", ] CTO_ADVISOR_REQUIRED_SCHEMA_FIELDS = { "status", "scalability_assessment", "strategic_risks", "strategic_verdict", "recommendation", "notes", } def _load_yaml(): return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8")) # =========================================================================== # 1. cto_advisor — регистрация в specialists.yaml # =========================================================================== class TestCtoAdvisorSpecialistsEntry: """cto_advisor зарегистрирован в specialists.yaml с корректной базовой структурой.""" def test_cto_advisor_exists_in_specialists(self): """cto_advisor присутствует в секции specialists.""" data = _load_yaml() assert "cto_advisor" in data.get("specialists", {}), ( "cto_advisor отсутствует в specialists.yaml" ) def test_cto_advisor_model_is_opus(self): """cto_advisor использует модель opus (стратегический ревьюер требует Opus).""" data = _load_yaml() role = data["specialists"]["cto_advisor"] assert role.get("model") == "opus", ( f"Ожидался model=opus, получили: {role.get('model')}" ) def test_cto_advisor_permissions_is_read_only(self): """cto_advisor имеет permissions=read_only (анализ без изменений).""" data = _load_yaml() role = data["specialists"]["cto_advisor"] assert role.get("permissions") == "read_only", ( f"Ожидался permissions=read_only, получили: {role.get('permissions')}" ) def test_cto_advisor_tools_include_read_grep_glob(self): """cto_advisor имеет инструменты Read, Grep, Glob.""" data = _load_yaml() tools = data["specialists"]["cto_advisor"].get("tools", []) for tool in ("Read", "Grep", "Glob"): assert tool in tools, f"cto_advisor должен иметь инструмент {tool!r}" def test_cto_advisor_has_output_schema(self): """cto_advisor имеет поле output_schema.""" data = _load_yaml() role = data["specialists"]["cto_advisor"] assert "output_schema" in role, "cto_advisor должен иметь output_schema" # =========================================================================== # 2. cto_advisor — output_schema поля # =========================================================================== class TestCtoAdvisorOutputSchemaFields: """output_schema cto_advisor содержит все обязательные поля.""" @pytest.mark.parametrize("required_field", sorted(CTO_ADVISOR_REQUIRED_SCHEMA_FIELDS)) def test_output_schema_contains_required_field(self, required_field): """output_schema cto_advisor содержит обязательное поле.""" data = _load_yaml() schema = data["specialists"]["cto_advisor"]["output_schema"] assert required_field in schema, ( f"output_schema cto_advisor обязана содержать поле {required_field!r}" ) # =========================================================================== # 3. cto_advisor — промпт существует и имеет стандартную структуру # =========================================================================== class TestCtoAdvisorPromptStructure: """agents/prompts/cto_advisor.md существует и содержит все 5 стандартных секций.""" def test_prompt_file_exists(self): """Файл agents/prompts/cto_advisor.md существует.""" assert CTO_ADVISOR_PROMPT.exists(), ( f"Промпт cto_advisor не найден: {CTO_ADVISOR_PROMPT}" ) def test_prompt_file_is_not_empty(self): """Файл cto_advisor.md не пустой.""" content = CTO_ADVISOR_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): """Промпт cto_advisor.md содержит каждую из 5 стандартных секций.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert section in content, ( f"cto_advisor.md не содержит обязательную секцию {section!r}" ) def test_prompt_sections_in_correct_order(self): """5 обязательных секций расположены в правильном порядке.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") positions = [content.find(sec) for sec in REQUIRED_SECTIONS] assert all(p != -1 for p in positions), "Не все 5 секций найдены в cto_advisor.md" assert positions == sorted(positions), ( f"Секции в cto_advisor.md расположены не по порядку. " f"Позиции: {dict(zip(REQUIRED_SECTIONS, positions))}" ) def test_prompt_has_input_section(self): """Промпт cto_advisor.md содержит секцию ## Input.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "## Input" in content, "cto_advisor.md не содержит секцию '## Input'" def test_prompt_contains_blocked_protocol(self): """Промпт cto_advisor.md содержит Blocked Protocol.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "blocked_at" in content, ( "cto_advisor.md не содержит 'blocked_at' — Blocked Protocol обязателен" ) # =========================================================================== # 4. cto_advisor — специфические поля выходной схемы в промпте # =========================================================================== class TestCtoAdvisorPromptOutputFields: """Промпт cto_advisor.md определяет ключевые поля стратегической оценки.""" def test_prompt_defines_scalability_assessment(self): """Промпт определяет поле 'scalability_assessment'.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "scalability_assessment" in content, ( "cto_advisor.md должен определять поле 'scalability_assessment'" ) def test_prompt_defines_strategic_verdict(self): """Промпт определяет поле 'strategic_verdict'.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "strategic_verdict" in content, ( "cto_advisor.md должен определять поле 'strategic_verdict'" ) def test_prompt_defines_strategic_risks(self): """Промпт определяет поле 'strategic_risks'.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "strategic_risks" in content, ( "cto_advisor.md должен определять поле 'strategic_risks'" ) def test_prompt_defines_platform_vs_product(self): """Промпт явно упоминает platform_vs_product.""" content = CTO_ADVISOR_PROMPT.read_text(encoding="utf-8") assert "platform_vs_product" in content, ( "cto_advisor.md должен упоминать поле 'platform_vs_product'" ) # =========================================================================== # 5. cto_advisor НЕ входит в department workers # =========================================================================== class TestCtoAdvisorNotInDepartments: """cto_advisor является опциональным специалистом — не входит в departments.""" def test_cto_advisor_not_in_any_department_workers(self): """cto_advisor не является обязательным членом ни одного департамента.""" data = _load_yaml() for dept_name, dept in data.get("departments", {}).items(): workers = dept.get("workers", []) assert "cto_advisor" not in workers, ( f"cto_advisor не должен быть в workers департамента '{dept_name}'. " "cto_advisor — опциональный стратегический ревьюер, не постоянный участник." )