Files
orchestrator/tests/test_orch_52b_docs_standard.py
claude-bot 5e60543232 docs(standards): pipeline docs standard — manifest + templates + ADR-naming
Создан golden source структуры номерных документов work item (ORCH-52b, слой 1
эпика ORCH-52). Docs-only: STAGE_TRANSITIONS / QG_CHECKS / check_* / схема БД не
трогаются (AC-6).

- docs/_standards/PIPELINE_DOCS.md — манифест «стадия→агент→документ→категория→
  гейт→frontmatter machine-key» (сверен с src/stages.py и src/qg/checks.py) +
  раздел ADR-naming. Манифест документирует поведение гейтов, источник истины
  остаётся код (ADR-001 §D2); честно различает machine-verdict (12/13/14/15/17)
  и информационные (00/08/10/16) доки; под-гейты ребра deploy-staging→deploy
  отмечены как врезки в advance_stage.
- docs/_templates/* — 15 копируемых скелетов; машинные доки несут точный
  frontmatter-ключ из _parse_* (verdict/result/deploy_status/staging_status/
  security_status/post_deploy_status).
- Точки-ссылки: CLAUDE.md, docs/architecture/README.md; запись CHANGELOG.
- tests/test_orch_52b_docs_standard.py — TC-01..TC-20 структурные проверки;
  полный pytest tests/ зелёный (1177 passed).

Refs: ORCH-075

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 13:25:39 +03:00

215 lines
11 KiB
Python
Raw Permalink 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.
"""ORCH-075 (ORCH-52b) — структурные проверки стандарта документов конвейера.
Docs-only задача: проверяется НАЛИЧИЕ и СТРУКТУРА новых файлов-стандартов/шаблонов
(`docs/_standards/PIPELINE_DOCS.md`, `docs/_templates/*`) и обновление точек-ссылок
(`CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md`). Тесты НЕ меняют
`QG_CHECKS`/`STAGE_TRANSITIONS` и не вводят новый гейт (это ORCH-52c).
Покрытие тест-плана 04-test-plan.yaml: TC-01…TC-20 (TC-21 — полный регресс `pytest tests/`).
"""
from pathlib import Path
import yaml
REPO_ROOT = Path(__file__).resolve().parents[1]
STANDARDS = REPO_ROOT / "docs" / "_standards"
TEMPLATES = REPO_ROOT / "docs" / "_templates"
MANIFEST = STANDARDS / "PIPELINE_DOCS.md"
# Все номерные доки реального набора (TRZ §FR-1, AC-1).
NUMBERED_DOCS = ["00", "01", "02", "03", "04", "06", "07", "08", "10",
"12", "13", "14", "15", "16", "17"]
# Шаблоны required/when-applicable доков (TRZ §2, AC-2).
TEMPLATE_FILES = [
"00-business-request.md",
"01-brd.md",
"02-trz.md",
"03-acceptance-criteria.md",
"04-test-plan.yaml",
"06-adr-ADR-NNN-slug.md",
"07-infra-requirements.md",
"08-data-requirements.md",
"10-tech-risks.md",
"12-review.md",
"13-test-report.md",
"14-deploy-log.md",
"15-staging-log.md",
"16-post-deploy-log.md",
"17-security-report.md",
]
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
# --- TC-01 -----------------------------------------------------------------
def test_tc01_manifest_exists_and_nonempty():
"""docs/_standards/PIPELINE_DOCS.md существует и непустой."""
assert MANIFEST.is_file(), f"манифест не найден: {MANIFEST}"
assert len(_read(MANIFEST).strip()) > 0, "манифест пустой"
# --- TC-02 -----------------------------------------------------------------
def test_tc02_manifest_mentions_all_numbered_docs():
"""Манифест упоминает все номерные доки набора."""
content = _read(MANIFEST)
missing = []
for num in NUMBERED_DOCS:
# Документ упоминается как `NN-...` или `NN-adr` — ищем по префиксу `NN-`.
if f"{num}-" not in content:
missing.append(num)
assert not missing, f"в манифесте не упомянуты доки: {missing}"
# --- TC-03 -----------------------------------------------------------------
def test_tc03_manifest_lists_owner_agents():
"""Манифест указывает владельцев-агентов конвейера (TC-03: analyst/architect/
reviewer/tester/deployer/система). `developer` не владеет номерным доком — пишет код+PR."""
content = _read(MANIFEST).lower()
for agent in ["analyst", "architect", "reviewer", "tester", "deployer"]:
assert agent in content, f"в манифесте нет владельца-агента: {agent}"
assert "систем" in content, "в манифесте нет системного владельца (Plane webhook)"
# --- TC-04 -----------------------------------------------------------------
def test_tc04_manifest_has_categories():
"""Манифест содержит категории required / when-applicable / optional."""
content = _read(MANIFEST)
for category in ["required", "when-applicable", "optional"]:
assert category in content, f"в манифесте нет категории: {category}"
# --- TC-05 -----------------------------------------------------------------
def test_tc05_templates_dir_has_all_templates():
"""docs/_templates/ существует и содержит шаблоны для всех required/when-applicable доков."""
assert TEMPLATES.is_dir(), f"каталог шаблонов не найден: {TEMPLATES}"
missing = [name for name in TEMPLATE_FILES if not (TEMPLATES / name).is_file()]
assert not missing, f"отсутствуют шаблоны: {missing}"
# --- TC-06..TC-11 — frontmatter machine-keys -------------------------------
def _assert_frontmatter_key(template_name: str, key: str):
content = _read(TEMPLATES / template_name)
assert content.lstrip().startswith("---"), f"{template_name}: нет YAML-frontmatter"
head = content.split("---", 2)
assert len(head) >= 3, f"{template_name}: frontmatter не закрыт"
assert f"{key}:" in head[1], f"{template_name}: нет machine-key `{key}:` во frontmatter"
def test_tc06_review_template_has_verdict():
"""Шаблон 12-review содержит frontmatter-ключ verdict:."""
_assert_frontmatter_key("12-review.md", "verdict")
def test_tc07_test_report_template_has_result():
"""Шаблон 13-test-report содержит frontmatter-ключ result:."""
_assert_frontmatter_key("13-test-report.md", "result")
def test_tc08_deploy_log_template_has_deploy_status():
"""Шаблон 14-deploy-log содержит frontmatter-ключ deploy_status:."""
_assert_frontmatter_key("14-deploy-log.md", "deploy_status")
def test_tc09_staging_log_template_has_staging_status():
"""Шаблон 15-staging-log содержит frontmatter-ключ staging_status:."""
_assert_frontmatter_key("15-staging-log.md", "staging_status")
def test_tc10_security_report_template_has_security_status():
"""Шаблон 17-security-report содержит frontmatter-ключ security_status:."""
_assert_frontmatter_key("17-security-report.md", "security_status")
def test_tc11_post_deploy_template_has_post_deploy_status():
"""Шаблон 16-post-deploy-log содержит frontmatter-ключ post_deploy_status:."""
_assert_frontmatter_key("16-post-deploy-log.md", "post_deploy_status")
# --- TC-12 -----------------------------------------------------------------
def test_tc12_brd_template_has_mandatory_sections():
"""Шаблон 01-brd содержит обязательные секции (TRZ §FR-2.1)."""
content = _read(TEMPLATES / "01-brd.md")
for section in ["Бизнес-контекст", "Объём", "Бизнес-требования", "Нефункциональные требования"]:
assert section in content, f"01-brd: нет секции `{section}`"
# --- TC-13 -----------------------------------------------------------------
def test_tc13_trz_template_has_mandatory_sections():
"""Шаблон 02-trz содержит обязательные секции (TRZ §FR-2.1)."""
content = _read(TEMPLATES / "02-trz.md")
for section in ["Задействованные модули", "Изменения API", "Изменения схемы БД",
"QG checks"]:
assert section in content, f"02-trz: нет секции `{section}`"
# --- TC-14 -----------------------------------------------------------------
def test_tc14_ac_template_has_pass_fail_block():
"""Шаблон 03-acceptance-criteria содержит блок AC-N с метками PASS и FAIL."""
content = _read(TEMPLATES / "03-acceptance-criteria.md")
assert "AC-1" in content, "03-ac: нет блока AC-N"
assert "**PASS:**" in content, "03-ac: нет метки PASS"
assert "**FAIL:**" in content, "03-ac: нет метки FAIL"
# --- TC-15 -----------------------------------------------------------------
def test_tc15_test_plan_template_is_valid_yaml():
"""Шаблон 04-test-plan.yaml — валидный YAML с work_item и списком tests."""
data = yaml.safe_load(_read(TEMPLATES / "04-test-plan.yaml"))
assert isinstance(data, dict), "04-test-plan: корень не словарь"
assert "work_item" in data, "04-test-plan: нет ключа work_item"
assert isinstance(data.get("tests"), list) and data["tests"], "04-test-plan: tests не список"
first = data["tests"][0]
for key in ["id", "type", "description", "module", "expected"]:
assert key in first, f"04-test-plan: в элементе tests нет ключа `{key}`"
# --- TC-16 -----------------------------------------------------------------
def test_tc16_adr_naming_section_present():
"""Раздел ADR-naming фиксирует формат ADR-NNN-<slug>.md с нумерацией с 001 и kebab-slug."""
content = _read(MANIFEST)
assert "ADR-NNN-" in content, "нет формата ADR-NNN-<slug>.md"
assert "06-adr" in content, "нет пути 06-adr/"
assert "001" in content, "не зафиксирована нумерация с 001"
assert "kebab" in content.lower(), "нет правила kebab-case для slug"
# --- TC-17 -----------------------------------------------------------------
def test_tc17_adr_naming_matches_real_repo():
"""ADR-naming совпадает с реальными ADR в репо (пример из манифеста существует)."""
# Манифест приводит ORCH-088 как пример; реальный файл должен существовать.
adr_dir = REPO_ROOT / "docs" / "work-items" / "ORCH-088" / "06-adr"
real = list(adr_dir.glob("ADR-001-*.md"))
assert real, f"реальный ADR-001-*.md не найден в {adr_dir}"
# И глобальный реестр следует 4-значной конвенции.
global_adr = REPO_ROOT / "docs" / "architecture" / "adr"
assert list(global_adr.glob("adr-0019-*.md")), "глобальный adr-0019-*.md не найден"
# --- TC-18 -----------------------------------------------------------------
def test_tc18_claude_md_links_standard():
"""CLAUDE.md содержит ссылку на docs/_standards/PIPELINE_DOCS.md."""
content = _read(REPO_ROOT / "CLAUDE.md")
assert "docs/_standards/PIPELINE_DOCS.md" in content
# --- TC-19 -----------------------------------------------------------------
def test_tc19_architecture_readme_links_standard():
"""docs/architecture/README.md содержит ссылку на стандарт документов."""
content = _read(REPO_ROOT / "docs" / "architecture" / "README.md")
assert "PIPELINE_DOCS.md" in content
# --- TC-20 -----------------------------------------------------------------
def test_tc20_changelog_mentions_task():
"""CHANGELOG.md содержит запись об ORCH-52b/ORCH-075 в разделе Unreleased."""
content = _read(REPO_ROOT / "CHANGELOG.md")
unreleased = content.split("## [Unreleased]", 1)
assert len(unreleased) == 2, "нет раздела ## [Unreleased]"
# Берём срез до следующего релизного заголовка, если он есть.
body = unreleased[1].split("\n## ", 1)[0]
assert "ORCH-075" in body or "ORCH-52b" in body, "в Unreleased нет записи ORCH-075/ORCH-52b"