From d61f0f2993b766978897d9ae9dd45a2a969010e1 Mon Sep 17 00:00:00 2001 From: Gros Frumos Date: Thu, 19 Mar 2026 20:56:12 +0200 Subject: [PATCH] kin: auto-commit after pipeline --- tests/test_kin_docs_006_regression.py | 297 ++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 tests/test_kin_docs_006_regression.py diff --git a/tests/test_kin_docs_006_regression.py b/tests/test_kin_docs_006_regression.py new file mode 100644 index 0000000..0df62a5 --- /dev/null +++ b/tests/test_kin_docs_006_regression.py @@ -0,0 +1,297 @@ +"""Regression tests for KIN-DOCS-006 — repo_researcher: no API fields in output schema. + +Acceptance criteria: +1. repo_researcher зарегистрирован в specialists.yaml с output_schema без API-полей +2. output_schema repo_researcher НЕ содержит полей: endpoints, rate_limits, auth_method +3. output_schema repo_researcher содержит поля основного отчёта: repo_overview, tech_stack, + architecture_summary, key_components, strengths, weaknesses, integration_points, gotchas, notes +4. agents/prompts/repo_researcher.md существует и содержит все 5 стандартных секций +5. Промпт repo_researcher НЕ содержит API-полей в Return Format +6. repo_researcher доступен в departments.research.workers +7. Регрессия: tech_researcher по-прежнему содержит свои API-поля (не сломан) +""" + +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" +REPO_RESEARCHER_PROMPT = PROMPTS_DIR / "repo_researcher.md" + +REQUIRED_SECTIONS = [ + "## Working Mode", + "## Focus On", + "## Quality Checks", + "## Return Format", + "## Constraints", +] + +# Поля, которые НЕ должны присутствовать в repo_researcher (API-специфичные поля) +API_FIELDS_FORBIDDEN = {"endpoints", "rate_limits", "auth_method"} + +# Поля, которые ОБЯЗАНЫ присутствовать в repo_researcher output_schema +REPO_RESEARCHER_REQUIRED_SCHEMA_FIELDS = { + "status", + "repo_overview", + "tech_stack", + "architecture_summary", + "key_components", + "strengths", + "weaknesses", + "integration_points", + "gotchas", + "notes", +} + +# API-поля, которые обязаны остаться в tech_researcher (регрессия) +TECH_RESEARCHER_API_FIELDS = {"endpoints", "rate_limits", "auth_method"} + + +def _load_yaml(): + return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8")) + + +# =========================================================================== +# 1. repo_researcher — YAML schema: отсутствие API-полей +# =========================================================================== + +class TestRepoResearcherOutputSchemaNoApiFields: + """output_schema repo_researcher НЕ должна содержать API-специфичные поля.""" + + @pytest.mark.parametrize("forbidden_field", sorted(API_FIELDS_FORBIDDEN)) + def test_output_schema_does_not_contain_api_field(self, forbidden_field): + """output_schema repo_researcher не содержит API-поле.""" + data = _load_yaml() + schema = data["specialists"]["repo_researcher"]["output_schema"] + assert forbidden_field not in schema, ( + f"output_schema repo_researcher не должна содержать поле {forbidden_field!r} — " + "это API-специфичное поле, принадлежащее tech_researcher" + ) + + +# =========================================================================== +# 2. repo_researcher — YAML schema: наличие полей отчёта +# =========================================================================== + +class TestRepoResearcherOutputSchemaRequiredFields: + """output_schema repo_researcher ДОЛЖНА содержать все поля основного отчёта.""" + + @pytest.mark.parametrize("required_field", sorted(REPO_RESEARCHER_REQUIRED_SCHEMA_FIELDS)) + def test_output_schema_contains_required_field(self, required_field): + """output_schema repo_researcher содержит обязательное поле отчёта.""" + data = _load_yaml() + schema = data["specialists"]["repo_researcher"]["output_schema"] + assert required_field in schema, ( + f"output_schema repo_researcher обязана содержать поле {required_field!r}" + ) + + +# =========================================================================== +# 3. repo_researcher — регистрация и базовая структура в specialists.yaml +# =========================================================================== + +class TestRepoResearcherSpecialistsEntry: + """repo_researcher зарегистрирован в specialists.yaml с корректной базовой структурой.""" + + def test_repo_researcher_exists_in_specialists(self): + """repo_researcher присутствует в секции specialists.""" + data = _load_yaml() + assert "repo_researcher" in data.get("specialists", {}), ( + "repo_researcher отсутствует в specialists.yaml" + ) + + def test_repo_researcher_model_is_sonnet(self): + """repo_researcher использует модель sonnet.""" + data = _load_yaml() + role = data["specialists"]["repo_researcher"] + assert role.get("model") == "sonnet", ( + f"Ожидался model=sonnet, получили: {role.get('model')}" + ) + + def test_repo_researcher_permissions_is_read_only(self): + """repo_researcher имеет permissions=read_only.""" + data = _load_yaml() + role = data["specialists"]["repo_researcher"] + assert role.get("permissions") == "read_only", ( + f"Ожидался permissions=read_only, получили: {role.get('permissions')}" + ) + + def test_repo_researcher_tools_include_read_grep_glob(self): + """repo_researcher имеет инструменты Read, Grep, Glob.""" + data = _load_yaml() + tools = data["specialists"]["repo_researcher"].get("tools", []) + for tool in ("Read", "Grep", "Glob"): + assert tool in tools, f"repo_researcher должен иметь инструмент {tool!r}" + + def test_repo_researcher_has_output_schema(self): + """repo_researcher имеет поле output_schema.""" + data = _load_yaml() + role = data["specialists"]["repo_researcher"] + assert "output_schema" in role, "repo_researcher должен иметь output_schema" + + +# =========================================================================== +# 4. repo_researcher промпт — существование и структура +# =========================================================================== + +class TestRepoResearcherPromptStructure: + """agents/prompts/repo_researcher.md существует и содержит все 5 стандартных секций.""" + + def test_prompt_file_exists(self): + """Файл agents/prompts/repo_researcher.md существует.""" + assert REPO_RESEARCHER_PROMPT.exists(), ( + f"Промпт repo_researcher не найден: {REPO_RESEARCHER_PROMPT}" + ) + + def test_prompt_file_is_not_empty(self): + """Файл repo_researcher.md не пустой.""" + content = REPO_RESEARCHER_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): + """Промпт repo_researcher.md содержит каждую из 5 стандартных секций.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert section in content, ( + f"repo_researcher.md не содержит обязательную секцию {section!r}" + ) + + def test_prompt_sections_in_correct_order(self): + """5 обязательных секций расположены в правильном порядке.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + positions = [content.find(sec) for sec in REQUIRED_SECTIONS] + assert all(p != -1 for p in positions), "Не все 5 секций найдены в repo_researcher.md" + assert positions == sorted(positions), ( + f"Секции в repo_researcher.md расположены не по порядку. " + f"Позиции: {dict(zip(REQUIRED_SECTIONS, positions))}" + ) + + def test_prompt_has_input_section(self): + """Промпт repo_researcher.md содержит секцию ## Input.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert "## Input" in content, "repo_researcher.md не содержит секцию '## Input'" + + def test_prompt_contains_blocked_protocol(self): + """Промпт repo_researcher.md содержит Blocked Protocol.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert "blocked_reason" in content, ( + "repo_researcher.md не содержит 'blocked_reason' — Blocked Protocol обязателен" + ) + + +# =========================================================================== +# 5. repo_researcher промпт — отсутствие API-полей +# =========================================================================== + +class TestRepoResearcherPromptNoApiFields: + """Промпт repo_researcher.md не упоминает API-специфичные поля в Return Format.""" + + def test_prompt_does_not_define_endpoints_field(self): + """Промпт repo_researcher.md не содержит поля 'endpoints' в выходной схеме.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + # endpoints может встречаться только в integration_points контексте, + # но не как самостоятельное JSON-поле вида "endpoints": + assert '"endpoints"' not in content, ( + "repo_researcher.md не должен определять JSON-поле 'endpoints' — " + "это API-специфичное поле tech_researcher" + ) + + def test_prompt_does_not_define_rate_limits_field(self): + """Промпт repo_researcher.md не содержит поля 'rate_limits' в выходной схеме.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert '"rate_limits"' not in content, ( + "repo_researcher.md не должен определять JSON-поле 'rate_limits' — " + "это API-специфичное поле tech_researcher" + ) + + def test_prompt_does_not_define_auth_method_field(self): + """Промпт repo_researcher.md не содержит поля 'auth_method' в выходной схеме.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert '"auth_method"' not in content, ( + "repo_researcher.md не должен определять JSON-поле 'auth_method' — " + "это API-специфичное поле tech_researcher" + ) + + def test_prompt_defines_repo_overview_field(self): + """Промпт repo_researcher.md определяет поле 'repo_overview'.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert "repo_overview" in content, ( + "repo_researcher.md должен определять поле 'repo_overview'" + ) + + def test_prompt_defines_architecture_summary_field(self): + """Промпт repo_researcher.md определяет поле 'architecture_summary'.""" + content = REPO_RESEARCHER_PROMPT.read_text(encoding="utf-8") + assert "architecture_summary" in content, ( + "repo_researcher.md должен определять поле 'architecture_summary'" + ) + + +# =========================================================================== +# 6. repo_researcher в departments.research +# =========================================================================== + +class TestRepoResearcherInResearchDepartment: + """repo_researcher доступен в departments.research.workers.""" + + def test_repo_researcher_in_research_workers(self): + """repo_researcher присутствует в departments.research.workers.""" + data = _load_yaml() + workers = data["departments"]["research"].get("workers", []) + assert "repo_researcher" in workers, ( + f"repo_researcher должен быть в departments.research.workers. " + f"Текущие workers: {workers}" + ) + + def test_research_head_describes_repo_researcher(self): + """research_head description упоминает repo_researcher.""" + data = _load_yaml() + description = data["specialists"]["research_head"].get("description", "") + assert "repo_researcher" in description, ( + "research_head description должен упоминать repo_researcher" + ) + + def test_tech_researcher_still_in_research_workers(self): + """Регрессия: tech_researcher по-прежнему в departments.research.workers.""" + data = _load_yaml() + workers = data["departments"]["research"].get("workers", []) + assert "tech_researcher" in workers, ( + "Регрессия: tech_researcher пропал из departments.research.workers" + ) + + +# =========================================================================== +# 7. Регрессия: tech_researcher не сломан (API-поля на месте) +# =========================================================================== + +class TestTechResearcherApiFieldsRegression: + """Регрессия: tech_researcher по-прежнему содержит API-специфичные поля.""" + + @pytest.mark.parametrize("api_field", sorted(TECH_RESEARCHER_API_FIELDS)) + def test_tech_researcher_output_schema_still_has_api_field(self, api_field): + """tech_researcher output_schema по-прежнему содержит API-поле (регрессия).""" + data = _load_yaml() + schema = data["specialists"]["tech_researcher"]["output_schema"] + assert api_field in schema, ( + f"Регрессия: tech_researcher output_schema потеряла поле {api_field!r}" + ) + + def test_tech_researcher_description_mentions_external_api(self): + """tech_researcher description явно указывает назначение: внешние API.""" + data = _load_yaml() + description = data["specialists"]["tech_researcher"].get("description", "").lower() + assert "external" in description or "api" in description, ( + "tech_researcher description должен упоминать внешние API" + ) + + def test_tech_researcher_description_mentions_repo_researcher(self): + """tech_researcher description упоминает repo_researcher для кодовых баз.""" + data = _load_yaml() + description = data["specialists"]["tech_researcher"].get("description", "") + assert "repo_researcher" in description, ( + "tech_researcher description должен упоминать repo_researcher " + "как альтернативу для анализа репозиториев" + )