"""ORCH-103 (TC-05/06/09/10/11, AC-4/AC-9): анти-дрейф golden source `docs/deployment/BUNDLED_SETUP.md` + секрет-гигиена новых артефактов bundle. Зеркало паттерна `tests/test_lite_setup_doc.py` (ORCH-102 D8): 14 нормативных разделов ADR-001 D10 в порядке маршрута оператора; обязательные кирпичи; «Требования к хосту» с цифрами, синхронизированными с константами preflight `scripts/bootstrap_bundle.py`; каждый упомянутый env-ключ существует в канонах (`.env.example` ∪ `deploy/bundled/.env.example`); гигиена FORBIDDEN (импорт из `tests/test_no_host_hardcodes.py` — один источник истины) и секрет-эвристика hex>=32 / alnum>=40 по доку и всем новым артефактам; «22 статуса» — сверкой импорта `plane_sync._PLANE_NAME_TO_KEY`, не литералом; кросс-ссылки канона. Детерминировано: без сети/LLM/subprocess/docker. """ import importlib.util import re from pathlib import Path # Один источник истины запрещённых боевых литералов (ORCH-101 AC-7). from tests.test_no_host_hardcodes import FORBIDDEN REPO_ROOT = Path(__file__).resolve().parents[1] DOC = REPO_ROOT / "docs/deployment/BUNDLED_SETUP.md" LITE_SETUP = REPO_ROOT / "docs/deployment/LITE_SETUP.md" REPLICATION = REPO_ROOT / "docs/operations/REPLICATION.md" CHANGELOG = REPO_ROOT / "CHANGELOG.md" ENV_EXAMPLE = REPO_ROOT / ".env.example" BUNDLE_ENV_EXAMPLE = REPO_ROOT / "deploy/bundled/.env.example" BUNDLE_COMPOSE = REPO_ROOT / "deploy/bundled/docker-compose.yml" BOOTSTRAP = REPO_ROOT / "scripts/bootstrap_bundle.py" # Нормативная структура ADR-001 D10: 14 разделов, порядок = маршрут оператора. SECTIONS: tuple[str, ...] = ( "## 1. Рамка Bundled", "## 2. Требования к хосту", "## 3. Предусловия", "## 4. Получение кода", "## 5. Секреты", "## 6. Запуск bundle-compose", "## 7. Bootstrap", "## 8. LLM (claude CLI)", "## 9. Telegram", "## 10. Онбординг следующих проектов", "## 11. Smoke", "## 12. Stateless-проверка", "## 13. Остановка и полный сброс", "## 14. Траблшутинг", ) # Обязательные кирпичи дока (FR-4; подстроки). BRICKS: tuple[str, ...] = ( "bootstrap_bundle.py", "gen_secrets.py", "onboard_project.py", "docker compose -f deploy/bundled/docker-compose.yml", "orchestrator-bundle", "/health", "/queue", "/metrics", "Confirm Deploy", "STOP", "ALLOWED_HOST_LIST", "14 контейнеров", "Проверка", "PASS", "FAIL", ) # env-токены дока: полные имена ключей платформы/bundle (анти-фантом, TC-09). _ENV_TOKEN_RE = re.compile(r"\b(?:ORCH|WATCHDOG|BUNDLE)_[A-Z0-9_]*[A-Z0-9]\b") # Секрет-эвристика (паттерн D8 ORCH-102): hex-run >= 32 / чистый alnum >= 40. _SECRET_HEX_RE = re.compile(r"\b[0-9a-fA-F]{32,}\b") _SECRET_ALNUM_RE = re.compile(r"\b[A-Za-z0-9]{40,}\b") def _doc_text() -> str: assert DOC.is_file(), "docs/deployment/BUNDLED_SETUP.md отсутствует (AC-4)" return DOC.read_text(encoding="utf-8") def _section_bodies() -> dict: text = _doc_text() bodies = {} for i, header in enumerate(SECTIONS): start = text.find(header) assert start != -1, f"раздел {header!r} отсутствует (D10)" end = text.find(SECTIONS[i + 1]) if i + 1 < len(SECTIONS) else len(text) bodies[header] = text[start:end] return bodies def _fenced_blocks(text: str) -> list: return re.findall(r"```[^\n]*\n(.*?)```", text, flags=re.DOTALL) def _env_keys(path: Path) -> set: keys = set() for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue keys.add(line.split("=", 1)[0].strip()) return keys def _bootstrap_module(): spec = importlib.util.spec_from_file_location("bootstrap_bundle", BOOTSTRAP) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod # --------------------------------------------------------------------------- # TC-05: 14 разделов в порядке + форма «команда + проверка» + цифры хоста. # --------------------------------------------------------------------------- def test_doc_exists_with_all_14_sections_in_order(): text = _doc_text() positions = [] for header in SECTIONS: idx = text.find(header) assert idx != -1, f"нормативный раздел {header!r} отсутствует (D10/FR-4)" positions.append(idx) assert positions == sorted(positions), ( "разделы BUNDLED_SETUP.md идут не в порядке маршрута оператора (D10)" ) def test_doc_carries_all_mandatory_bricks(): text = _doc_text() missing = [b for b in BRICKS if b not in text] assert not missing, f"обязательные кирпичи отсутствуют (FR-4): {missing}" def test_every_executable_section_carries_commands(): """§2–§14 несут минимум одну fenced-команду; §1 (рамка) — без команд.""" bodies = _section_bodies() for header in SECTIONS[1:]: assert "```" in bodies[header], f"{header}: нет ни одной fenced-команды (D10)" def test_doc_carries_explicit_check_markers(): text = _doc_text() assert text.count("Проверка") >= 13, ( "маркеров «Проверка» меньше, чем исполняемых разделов (форма D10)" ) assert "PASS" in text and "FAIL" in text def test_host_requirements_carry_measured_numbers_synced_with_preflight(): """AC-4: «Требования к хосту» с явными цифрами RAM/диск/CPU и картой портов; цифры = константам preflight bootstrap (D5: синхронизированы механически).""" mod = _bootstrap_module() body = _section_bodies()["## 2. Требования к хосту"] assert f"{mod.MIN_RAM_GB} GB" in body, "цифра RAM разъехалась с MIN_RAM_GB" assert f"{mod.MIN_DISK_GB} GB" in body, "цифра диска разъехалась с MIN_DISK_GB" assert f"{mod.MIN_CPUS} vCPU" in body, "цифра CPU разъехалась с MIN_CPUS" for port in ("8500", "8080", "3000"): assert port in body, f"карта портов неполна: нет {port}" assert "14 контейнеров" in body or "14 контейнеров" in _doc_text() def test_doc_has_stateless_normative_line(): low = _doc_text().lower() assert "не перенос" in low, ( "нормативная stateless-строка («…боевого хоста НЕ переносятся») " "отсутствует (AC-3)" ) stateless = _section_bodies()["## 12. Stateless-проверка"] assert "/queue" in stateless, "§12 обязан нести проверку чистоты через GET /queue" def test_teardown_is_documented_procedure(): """D9: полный сброс — документированная процедура §13 (не режим скрипта).""" teardown = _section_bodies()["## 13. Остановка и полный сброс"] assert "down -v" in teardown, "§13 обязан нести полный сброс (down -v)" assert "НЕОБРАТИМО" in teardown or "необратим" in teardown.lower() def test_troubleshooting_covers_mandatory_symptoms(): """FR-4 п.14: webhook, RAM/OOM, порт занят, claude, миграции Plane.""" tr = _section_bodies()["## 14. Траблшутинг"] for needle in ("ebhook", "OOM", "орт занят", "claude", "играции"): assert needle in tr, f"траблшутинг не покрывает симптом: {needle!r}" assert "ALLOWED_HOST_LIST" in tr # мина Gitea — явно (D10) # --------------------------------------------------------------------------- # TC-06: гигиена новых артефактов — FORBIDDEN (импорт) + секрет-эвристика. # --------------------------------------------------------------------------- def _new_artifact_texts() -> dict: return { "BUNDLED_SETUP.md (fenced)": "\n".join(_fenced_blocks(_doc_text())), "deploy/bundled/docker-compose.yml": BUNDLE_COMPOSE.read_text(encoding="utf-8"), "deploy/bundled/.env.example": BUNDLE_ENV_EXAMPLE.read_text(encoding="utf-8"), "scripts/bootstrap_bundle.py": BOOTSTRAP.read_text(encoding="utf-8"), } def test_new_artifacts_carry_no_forbidden_literals(): offenders = [ f"{label}: {literal!r}" for label, text in _new_artifact_texts().items() for literal in FORBIDDEN if literal in text ] assert not offenders, ( "боевые литералы в артефактах bundle (NFR-3/TC-06):\n" + "\n".join(offenders) ) def test_new_artifacts_carry_no_secret_like_values(): offenders = [] for label, text in _new_artifact_texts().items(): for rx in (_SECRET_HEX_RE, _SECRET_ALNUM_RE): m = rx.search(text) if m is not None: offenders.append(f"{label}: {m.group(0)[:16]}…") assert not offenders, ( "секретоподобные значения в артефактах bundle (NFR-3):\n" + "\n".join(offenders) ) def test_secret_heuristic_is_not_evergreen(): """Негативный самочек (паттерн ORCH-101/102): эвристика реально ловит.""" assert _SECRET_HEX_RE.search("KEY=" + "0fa1" * 16) is not None assert _SECRET_ALNUM_RE.search("token" + "Ab1" * 15) is not None assert _SECRET_HEX_RE.search("curl -fsS http://127.0.0.1:8500/health") is None assert _SECRET_ALNUM_RE.search(" $ORCH_PLANE_API_TOKEN") is None # --------------------------------------------------------------------------- # TC-09: env-канон без фантомов + число статусов сверкой импорта. # --------------------------------------------------------------------------- def test_every_env_token_in_doc_exists_in_canons(): canon = _env_keys(ENV_EXAMPLE) | _env_keys(BUNDLE_ENV_EXAMPLE) mentioned = set(_ENV_TOKEN_RE.findall(_doc_text())) assert mentioned, "в BUNDLED_SETUP.md не упомянут ни один env-ключ — док не полон" unknown = sorted(mentioned - canon) assert not unknown, ( f"ключи из BUNDLED_SETUP.md отсутствуют в канонах (.env.example ∪ " f"deploy/bundled/.env.example) — опечатка или дрейф (TC-09): {unknown}" ) def test_status_count_claim_matches_plane_sync(): """«22 статуса» держится фактическим маппингом src/plane_sync.py (AC-7: сверка импортом, не строковой копией).""" from src.plane_sync import _PLANE_NAME_TO_KEY assert len(_PLANE_NAME_TO_KEY) == 22, ( "число статусов в plane_sync изменилось — обнови BUNDLED_SETUP.md §7 " "(и ONBOARDING.md §1)" ) assert "Confirm Deploy" in _PLANE_NAME_TO_KEY assert "STOP" in _PLANE_NAME_TO_KEY assert "22" in _doc_text(), "число статусов в BUNDLED_SETUP.md разъехалось с plane_sync" # --------------------------------------------------------------------------- # TC-10: канон не форкается — кросс-ссылки; REPLICATION §1 отмечает Type B. # --------------------------------------------------------------------------- def test_doc_links_canons_instead_of_forking(): text = _doc_text() for canon in ("LITE_SETUP.md", "ONBOARDING.md", "REPLICATION.md"): assert canon in text, f"BUNDLED_SETUP.md не ссылается на канон {canon} (FR-4)" bodies = _section_bodies() assert "LITE_SETUP.md" in bodies["## 8. LLM (claude CLI)"], "§8 — ссылкой на LITE_SETUP §7" assert "LITE_SETUP.md" in bodies["## 9. Telegram"], "§9 — ссылкой на LITE_SETUP §8" assert "ONBOARDING.md" in bodies["## 10. Онбординг следующих проектов"] assert "REPLICATION.md" in bodies["## 11. Smoke"], "smoke — на REPLICATION §4, без форка" def test_replication_marks_type_b_done(): text = REPLICATION.read_text(encoding="utf-8") assert "BUNDLED_SETUP.md" in text, ( "REPLICATION.md §1 обязан ссылаться на BUNDLED_SETUP.md (Type B реализован)" ) assert "ORCH-103" in text, "строка Type B в REPLICATION.md §1 не отмечена ✅ ORCH-103" def test_lite_setup_untouched_reference_exists(): """Канон Lite остаётся на месте (Bundled его дополняет, не заменяет).""" assert LITE_SETUP.is_file() # --------------------------------------------------------------------------- # TC-11: CHANGELOG. # --------------------------------------------------------------------------- def test_changelog_has_orch_103_entry(): assert "ORCH-103" in CHANGELOG.read_text(encoding="utf-8")