kin/tests/test_kin_docs_007_regression.py
2026-03-19 21:08:09 +02:00

207 lines
9.7 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-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 — опциональный стратегический ревьюер, не постоянный участник."
)