From 2053a9d26cf2d6feb72ab96ffacda8846c98f42f Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Thu, 19 Mar 2026 21:08:09 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_kin_docs_002_regression.py | 8 +- tests/test_kin_docs_007_regression.py | 207 ++++++++++++++++++++++++++ 2 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 tests/test_kin_docs_007_regression.py diff --git a/tests/test_kin_docs_002_regression.py b/tests/test_kin_docs_002_regression.py index 2023e0c..b746dec 100644 --- a/tests/test_kin_docs_002_regression.py +++ b/tests/test_kin_docs_002_regression.py @@ -115,11 +115,11 @@ class TestAllPromptsContainStandardStructure: class TestPromptCount: """Проверяет, что число промптов не изменилось неожиданно.""" - def test_prompt_count_is_28(self): - """В agents/prompts/ ровно 28 файлов .md.""" + def test_prompt_count_is_29(self): + """В agents/prompts/ ровно 29 файлов .md.""" count = len(_prompt_files()) - assert count == 28, ( # 28 промптов — актуально на 2026-03-19, +repo_researcher (KIN-DOCS-006, см. git log agents/prompts/) - f"Ожидалось 28 промптов, найдено {count}. " + assert count == 29, ( # 29 промптов — актуально на 2026-03-19, +cto_advisor (KIN-DOCS-007, см. git log agents/prompts/) + f"Ожидалось 29 промптов, найдено {count}. " "Если добавлен новый промпт — обнови этот тест." ) diff --git a/tests/test_kin_docs_007_regression.py b/tests/test_kin_docs_007_regression.py new file mode 100644 index 0000000..d7df58d --- /dev/null +++ b/tests/test_kin_docs_007_regression.py @@ -0,0 +1,207 @@ +"""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 — опциональный стратегический ревьюер, не постоянный участник." + )