Files
orchestrator/tests/test_bundled_setup_doc.py

292 lines
14 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-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("<ORCHESTRATOR_GIT_URL> $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")