2026-03-19 14:58:50 +02:00
|
|
|
|
"""Regression tests for KIN-DOCS-002 — Standardise all agent prompts.
|
|
|
|
|
|
|
|
|
|
|
|
Acceptance criteria:
|
|
|
|
|
|
1. pytest green (checked by running this suite)
|
|
|
|
|
|
2. No file in agents/prompts/ contains the old '## Output format' section header
|
|
|
|
|
|
3. Every prompt file contains '## Return Format'
|
|
|
|
|
|
4. Every prompt file contains the full standard structure:
|
2026-03-19 15:45:08 +02:00
|
|
|
|
## Working Mode, ## Focus On, ## Quality Checks, ## Return Format, ## Constraints
|
2026-03-19 14:58:50 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
PROMPTS_DIR = Path(__file__).parent.parent / "agents" / "prompts"
|
|
|
|
|
|
|
2026-03-19 15:45:08 +02:00
|
|
|
|
# Single source of truth for required sections (decision #920)
|
|
|
|
|
|
REQUIRED_SECTIONS = [
|
|
|
|
|
|
"## Working Mode",
|
|
|
|
|
|
"## Focus On",
|
|
|
|
|
|
"## Quality Checks",
|
|
|
|
|
|
"## Return Format",
|
|
|
|
|
|
"## Constraints",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# Files excluded from standard-structure checks (decision #917/#918)
|
2026-03-19 16:03:02 +02:00
|
|
|
|
# All 9 previously excluded files now contain all 5 required sections — list is empty.
|
|
|
|
|
|
# Guard-тест TestExclusionListIsEmpty (decision #929) не даст тихо добавить файлы обратно.
|
|
|
|
|
|
EXCLUDED_FROM_STRUCTURE_CHECK = []
|
2026-03-19 15:45:08 +02:00
|
|
|
|
|
2026-03-19 14:58:50 +02:00
|
|
|
|
|
|
|
|
|
|
def _prompt_files():
|
|
|
|
|
|
return sorted(PROMPTS_DIR.glob("*.md"))
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 15:45:08 +02:00
|
|
|
|
def _active_prompt_files():
|
|
|
|
|
|
"""Prompt files not in the exclusion list (decision #918)."""
|
|
|
|
|
|
return [f for f in _prompt_files() if f.name not in EXCLUDED_FROM_STRUCTURE_CHECK]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_ACTIVE_PROMPT_NAMES = [f.name for f in _active_prompt_files()]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-19 14:58:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# AC-2: No legacy '## Output format' section
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestNoLegacyOutputFormatSection:
|
|
|
|
|
|
"""Проверяет отсутствие устаревшей секции '## Output format' во всех промптах."""
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_prompt_contains_old_output_format_header(self):
|
|
|
|
|
|
"""Ни один файл agents/prompts/*.md не содержит '## Output format'."""
|
|
|
|
|
|
files_with_old_header = [
|
|
|
|
|
|
f.name
|
|
|
|
|
|
for f in _prompt_files()
|
|
|
|
|
|
if "## Output format" in f.read_text(encoding="utf-8")
|
|
|
|
|
|
]
|
|
|
|
|
|
assert files_with_old_header == [], (
|
|
|
|
|
|
f"Файлы с устаревшей секцией '## Output format': {files_with_old_header}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# AC-3: Every prompt contains '## Return Format'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestAllPromptsContainReturnFormat:
|
|
|
|
|
|
"""Проверяет наличие секции '## Return Format' во всех промптах."""
|
|
|
|
|
|
|
|
|
|
|
|
def test_return_format_count_equals_prompt_count(self):
|
|
|
|
|
|
"""Число промптов с '## Return Format' равно общему числу промптов."""
|
|
|
|
|
|
all_files = _prompt_files()
|
|
|
|
|
|
files_with_rf = [f for f in all_files if "## Return Format" in f.read_text(encoding="utf-8")]
|
|
|
|
|
|
assert len(files_with_rf) == len(all_files), (
|
|
|
|
|
|
f"Промптов всего: {len(all_files)}, "
|
|
|
|
|
|
f"с '## Return Format': {len(files_with_rf)}. "
|
|
|
|
|
|
f"Без секции: {[f.name for f in all_files if f not in files_with_rf]}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-03-19 18:51:19 +02:00
|
|
|
|
@pytest.mark.parametrize("prompt_file", _ACTIVE_PROMPT_NAMES)
|
2026-03-19 14:58:50 +02:00
|
|
|
|
def test_each_prompt_has_return_format(self, prompt_file):
|
|
|
|
|
|
"""Каждый промпт-файл содержит секцию '## Return Format'."""
|
|
|
|
|
|
content = (PROMPTS_DIR / prompt_file).read_text(encoding="utf-8")
|
|
|
|
|
|
assert "## Return Format" in content, (
|
|
|
|
|
|
f"{prompt_file} не содержит секцию '## Return Format'"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
2026-03-19 15:45:08 +02:00
|
|
|
|
# AC-4: Full standard structure in every active prompt (decision #917-#920)
|
2026-03-19 14:58:50 +02:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestAllPromptsContainStandardStructure:
|
2026-03-19 15:45:08 +02:00
|
|
|
|
"""Проверяет наличие полного набора обязательных секций во всех активных промптах.
|
2026-03-19 14:58:50 +02:00
|
|
|
|
|
2026-03-19 15:45:08 +02:00
|
|
|
|
Requirements:
|
|
|
|
|
|
1. Каждый активный промпт содержит все 5 секций из REQUIRED_SECTIONS в правильном порядке
|
|
|
|
|
|
"""
|
2026-03-19 14:58:50 +02:00
|
|
|
|
|
2026-03-19 15:45:08 +02:00
|
|
|
|
@pytest.mark.parametrize("section", REQUIRED_SECTIONS)
|
|
|
|
|
|
@pytest.mark.parametrize("prompt_file", _ACTIVE_PROMPT_NAMES)
|
|
|
|
|
|
def test_prompt_has_required_section(self, section, prompt_file):
|
|
|
|
|
|
"""Каждый активный промпт содержит каждую из 5 обязательных секций."""
|
2026-03-19 14:58:50 +02:00
|
|
|
|
content = (PROMPTS_DIR / prompt_file).read_text(encoding="utf-8")
|
2026-03-19 15:45:08 +02:00
|
|
|
|
assert section in content, (
|
|
|
|
|
|
f"{prompt_file!r} не содержит обязательную секцию {section!r}"
|
2026-03-19 14:58:50 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Sanity: prompt count stays at 25
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestPromptCount:
|
|
|
|
|
|
"""Проверяет, что число промптов не изменилось неожиданно."""
|
|
|
|
|
|
|
2026-03-19 20:52:34 +02:00
|
|
|
|
def test_prompt_count_is_28(self):
|
|
|
|
|
|
"""В agents/prompts/ ровно 28 файлов .md."""
|
2026-03-19 14:58:50 +02:00
|
|
|
|
count = len(_prompt_files())
|
2026-03-19 20:52:34 +02:00
|
|
|
|
assert count == 28, ( # 28 промптов — актуально на 2026-03-19, +repo_researcher (KIN-DOCS-006, см. git log agents/prompts/)
|
|
|
|
|
|
f"Ожидалось 28 промптов, найдено {count}. "
|
2026-03-19 14:58:50 +02:00
|
|
|
|
"Если добавлен новый промпт — обнови этот тест."
|
|
|
|
|
|
)
|
2026-03-19 16:03:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Guard: exclusion list must stay empty (decision #929)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestExclusionListIsEmpty:
|
|
|
|
|
|
"""Регрессионный guard против молчаливого роста EXCLUDED_FROM_STRUCTURE_CHECK.
|
|
|
|
|
|
|
|
|
|
|
|
Если нужно добавить файл обратно в exclusion — этот тест заставит явно
|
|
|
|
|
|
обосновать причину и обновить его (decision #929).
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def test_exclusion_list_is_empty(self):
|
|
|
|
|
|
"""EXCLUDED_FROM_STRUCTURE_CHECK должен оставаться пустым.
|
|
|
|
|
|
|
|
|
|
|
|
Все 9 ранее excluded файлов содержат все 5 стандартных секций.
|
|
|
|
|
|
Добавление файла в exclusion лишает его регрессионной защиты (decision #921).
|
|
|
|
|
|
Чтобы добавить файл — сначала обоснуй причину и обнови этот тест.
|
|
|
|
|
|
"""
|
|
|
|
|
|
assert EXCLUDED_FROM_STRUCTURE_CHECK == [], (
|
|
|
|
|
|
f"EXCLUDED_FROM_STRUCTURE_CHECK должен быть пустым, "
|
|
|
|
|
|
f"но содержит: {EXCLUDED_FROM_STRUCTURE_CHECK}. "
|
|
|
|
|
|
"Добавление файла в exclusion лишает его регрессионной защиты (decision #921)."
|
|
|
|
|
|
)
|