kin/tests/test_kin_docs_006_regression.py
2026-03-19 20:56:12 +02:00

297 lines
15 KiB
Python
Raw 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-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 "
"как альтернативу для анализа репозиториев"
)