kin/tests/test_kin_docs_005_regression.py
2026-03-19 20:46:08 +02:00

259 lines
12 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-005 — prompt_engineer role for AI projects.
Acceptance criteria:
1. specialists.yaml парсится без ошибок; роль prompt_engineer содержит все обязательные поля
2. agents/prompts/prompt_engineer.md содержит ровно 5 обязательных секций в правильном порядке (#940)
3. Роль prompt_engineer доступна в research department (departments.research.workers)
4. Регрессионный тест на наличие роли в списке specialists
"""
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"
REQUIRED_SECTIONS = [
"## Working Mode",
"## Focus On",
"## Quality Checks",
"## Return Format",
"## Constraints",
]
OUTPUT_SCHEMA_FIELDS = [
"status",
"prompt_design",
"quality_evaluation",
"model_recommendation",
"notes",
]
def _load_yaml():
return yaml.safe_load(SPECIALISTS_YAML.read_text(encoding="utf-8"))
# ===========================================================================
# 1. Структурный тест agents/specialists.yaml — роль prompt_engineer
# ===========================================================================
class TestPromptEngineerSpecialists:
"""Тесты регистрации prompt_engineer в agents/specialists.yaml."""
def test_role_exists_in_specialists(self):
"""specialists.yaml содержит роль prompt_engineer."""
data = _load_yaml()
assert "prompt_engineer" in data.get("specialists", {}), (
"prompt_engineer отсутствует в specialists.yaml"
)
def test_role_model_is_sonnet(self):
"""prompt_engineer использует модель sonnet."""
data = _load_yaml()
role = data["specialists"]["prompt_engineer"]
assert role.get("model") == "sonnet", (
f"Ожидался model=sonnet, получили: {role.get('model')}"
)
def test_role_tools_include_read_grep_glob(self):
"""prompt_engineer имеет инструменты Read, Grep, Glob."""
data = _load_yaml()
tools = data["specialists"]["prompt_engineer"].get("tools", [])
for required_tool in ("Read", "Grep", "Glob"):
assert required_tool in tools, (
f"prompt_engineer должен иметь инструмент {required_tool!r}"
)
def test_role_has_no_write_tools(self):
"""prompt_engineer НЕ имеет write-инструментов (read-only роль)."""
data = _load_yaml()
tools = set(data["specialists"]["prompt_engineer"].get("tools", []))
write_tools = {"Write", "Edit", "Bash"}
unexpected = write_tools & tools
assert not unexpected, (
f"prompt_engineer не должен иметь write-инструменты: {unexpected}"
)
def test_role_permissions_is_read_only(self):
"""prompt_engineer имеет permissions=read_only."""
data = _load_yaml()
role = data["specialists"]["prompt_engineer"]
assert role.get("permissions") == "read_only", (
f"Ожидался permissions=read_only, получили: {role.get('permissions')}"
)
def test_role_has_output_schema(self):
"""prompt_engineer имеет поле output_schema."""
data = _load_yaml()
role = data["specialists"]["prompt_engineer"]
assert "output_schema" in role, (
"prompt_engineer должен иметь output_schema"
)
@pytest.mark.parametrize("field", OUTPUT_SCHEMA_FIELDS)
def test_output_schema_has_required_field(self, field):
"""output_schema содержит каждое из обязательных полей."""
data = _load_yaml()
schema = data["specialists"]["prompt_engineer"]["output_schema"]
assert field in schema, (
f"output_schema prompt_engineer не содержит обязательного поля {field!r}"
)
def test_yaml_parses_without_error(self):
"""specialists.yaml парсится без ошибок (yaml.safe_load не бросает исключений)."""
data = _load_yaml()
assert isinstance(data, dict), "specialists.yaml не вернул dict при парсинге"
assert "specialists" in data, "specialists.yaml не содержит секцию 'specialists'"
def test_role_context_rules_decisions_all(self):
"""prompt_engineer получает все decisions (context_rules.decisions=all)."""
data = _load_yaml()
role = data["specialists"]["prompt_engineer"]
decisions = role.get("context_rules", {}).get("decisions")
assert decisions == "all", (
f"Ожидался context_rules.decisions=all, получили: {decisions}"
)
# ===========================================================================
# 2. Структурный тест agents/prompts/prompt_engineer.md
# ===========================================================================
class TestPromptEngineerPrompt:
"""Структурный тест agents/prompts/prompt_engineer.md (#940)."""
def test_prompt_file_exists(self):
"""Файл agents/prompts/prompt_engineer.md существует."""
path = PROMPTS_DIR / "prompt_engineer.md"
assert path.exists(), "prompt_engineer.md не найден в agents/prompts/"
@pytest.mark.parametrize("section", REQUIRED_SECTIONS)
def test_prompt_has_required_section(self, section):
"""Промпт содержит все 5 обязательных секций (REQUIRED_SECTIONS)."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert section in content, (
f"prompt_engineer.md не содержит обязательную секцию {section!r}"
)
def test_prompt_sections_in_correct_order(self):
"""5 обязательных секций расположены в правильном порядке в prompt_engineer.md."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
positions = [content.find(sec) for sec in REQUIRED_SECTIONS]
assert all(p != -1 for p in positions), (
"Не все 5 секций найдены в prompt_engineer.md"
)
assert positions == sorted(positions), (
f"Секции расположены не по порядку. Позиции: "
f"{dict(zip(REQUIRED_SECTIONS, positions))}"
)
def test_prompt_has_input_section(self):
"""Промпт содержит секцию ## Input — агент-специфичная секция."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "## Input" in content, (
"prompt_engineer.md не содержит секцию '## Input'"
)
def test_prompt_contains_blocked_protocol(self):
"""Промпт содержит Blocked Protocol с инструкцией blocked_reason."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "blocked_reason" in content, (
"prompt_engineer.md не содержит 'blocked_reason' — Blocked Protocol обязателен"
)
def test_prompt_no_legacy_output_format_header(self):
"""Промпт НЕ содержит устаревшей секции '## Output format'."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "## Output format" not in content, (
"prompt_engineer.md содержит устаревшую секцию '## Output format'"
)
def test_prompt_contains_prompt_design_field(self):
"""Промпт упоминает поле prompt_design в Return Format."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "prompt_design" in content, (
"prompt_engineer.md не содержит поля 'prompt_design'"
)
def test_prompt_contains_quality_evaluation_field(self):
"""Промпт упоминает поле quality_evaluation в Return Format."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "quality_evaluation" in content, (
"prompt_engineer.md не содержит поля 'quality_evaluation'"
)
def test_prompt_contains_model_recommendation_field(self):
"""Промпт упоминает поле model_recommendation в Return Format."""
content = (PROMPTS_DIR / "prompt_engineer.md").read_text(encoding="utf-8")
assert "model_recommendation" in content, (
"prompt_engineer.md не содержит поля 'model_recommendation'"
)
# ===========================================================================
# 3. Роль доступна в research department
# ===========================================================================
class TestPromptEngineerInResearchDepartment:
"""Тесты доступности prompt_engineer в departments.research."""
def test_research_department_exists(self):
"""departments.research существует в specialists.yaml."""
data = _load_yaml()
assert "research" in data.get("departments", {}), (
"departments.research отсутствует в specialists.yaml"
)
def test_prompt_engineer_in_research_workers(self):
"""prompt_engineer присутствует в departments.research.workers."""
data = _load_yaml()
workers = data["departments"]["research"].get("workers", [])
assert "prompt_engineer" in workers, (
f"prompt_engineer должен быть в departments.research.workers. "
f"Текущие workers: {workers}"
)
def test_research_head_describes_prompt_engineer(self):
"""research_head description упоминает prompt_engineer."""
data = _load_yaml()
description = data["specialists"]["research_head"].get("description", "")
assert "prompt_engineer" in description, (
"research_head description должен упоминать prompt_engineer"
)
def test_research_workers_include_tech_researcher_and_architect(self):
"""departments.research.workers по-прежнему содержит tech_researcher и architect (регрессия)."""
data = _load_yaml()
workers = data["departments"]["research"].get("workers", [])
for existing_role in ("tech_researcher", "architect"):
assert existing_role in workers, (
f"Регрессия: {existing_role!r} пропал из departments.research.workers"
)
# ===========================================================================
# 4. Регрессионный тест: наличие роли в списке specialists
# ===========================================================================
class TestPromptEngineerRoleRegistration:
"""Регрессионный тест: prompt_engineer зарегистрирован в specialists."""
def test_prompt_engineer_in_specialists_list(self):
"""prompt_engineer присутствует в секции specialists файла specialists.yaml."""
data = _load_yaml()
specialist_roles = list(data.get("specialists", {}).keys())
assert "prompt_engineer" in specialist_roles, (
f"prompt_engineer отсутствует в списке specialists. "
f"Текущие роли: {specialist_roles}"
)
def test_prompt_engineer_not_in_exclusion_list(self):
"""prompt_engineer.md не включён в EXCLUDED_FROM_STRUCTURE_CHECK."""
from tests.test_kin_docs_002_regression import EXCLUDED_FROM_STRUCTURE_CHECK
assert "prompt_engineer.md" not in EXCLUDED_FROM_STRUCTURE_CHECK, (
"prompt_engineer.md не должен быть в EXCLUDED_FROM_STRUCTURE_CHECK — "
"роль должна проходить все стандартные структурные проверки"
)