"""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-.md с нумерацией с 001 и kebab-slug.""" content = _read(MANIFEST) assert "ADR-NNN-" in content, "нет формата ADR-NNN-.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"