259 lines
12 KiB
Python
259 lines
12 KiB
Python
"""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 — "
|
||
"роль должна проходить все стандартные структурные проверки"
|
||
)
|