developer(ET): auto-commit from developer run_id=627

This commit is contained in:
2026-06-11 01:53:21 +03:00
committed by orchestrator-deployer
parent 054b78c8ca
commit 215930fb90
6 changed files with 1244 additions and 3 deletions

View File

@@ -0,0 +1,247 @@
"""ORCH-103 (TC-07/TC-08, AC-7/AC-8): структурные и unit-проверки
`scripts/bootstrap_bundle.py`.
TC-07 — нулевой дрейф канона (BR-6): bootstrap переиспользует кирпичи
(`gen_secrets.py` — webhook-секреты, `onboard_project.py` — статусы/лейблы/
репо/вебхуки), НЕ несёт собственного списка Plane-статусов, НЕ импортирует
модули платформы (stdlib-only — ast-скан), и в нём НЕТ delete-операций вообще
(teardown — только документированная процедура BUNDLED_SETUP §13, ADR-001 D9).
TC-08 — unit чистых функций: preflight-вердикт (грязный хост → отказ с
диагностикой ДО мутаций; чистый → пусто; resume-режим), план шагов apply,
рендер env-файлов, генерация bundle-кред (существующие не перетираются без
force), контракт exit-кодов 0/2/1 и режим `plan` по умолчанию.
Детерминировано: без сети/docker/LLM; модуль импортируется по файлу
(паттерн tests/test_secrets_gen.py), его import не имеет side effects.
"""
import ast
import importlib.util
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = REPO_ROOT / "scripts/bootstrap_bundle.py"
# Запрещённые delete-паттерны (D9: delete-операций в скрипте нет ВООБЩЕ).
FORBIDDEN_DELETE_NEEDLES = (
"volume rm",
"rm -rf",
"down -v",
"compose down",
"rmtree",
"os.remove",
".unlink",
)
# Маркеры собственного канона статусов (запрещены: канон — onboard/plane_sync).
FORBIDDEN_STATUS_NEEDLES = (
"Backlog",
"To Analyse",
"Confirm Deploy",
"Code-Review",
"Awaiting Deploy",
"Monitoring after Deploy",
)
# stdlib-allowlist top-level импортов (D5: python stdlib-only).
STDLIB_ALLOWED = {
"argparse", "dataclasses", "getpass", "json", "os", "pathlib", "re",
"secrets", "shutil", "socket", "subprocess", "sys", "tempfile", "time",
"urllib", "uuid",
}
def _source() -> str:
assert SCRIPT.is_file(), "scripts/bootstrap_bundle.py отсутствует (FR-2)"
return SCRIPT.read_text(encoding="utf-8")
def _load_module():
spec = importlib.util.spec_from_file_location("bootstrap_bundle", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
# ---------------------------------------------------------------------------
# TC-07: кирпичи переиспользованы, дрейфа канона нет, delete-операций нет.
# ---------------------------------------------------------------------------
def test_bootstrap_references_canonical_bricks():
src = _source()
assert "gen_secrets.py" in src, "webhook-секреты обязаны идти через gen_secrets.py (AC-7)"
assert "onboard_project.py" in src, "онбординг обязан идти через onboard_project.py (AC-7)"
def test_bootstrap_does_not_import_platform_modules():
src = _source()
assert "from src" not in src and "import src" not in src, (
"bootstrap обязан быть stdlib-only без импортов платформы (D5)"
)
def test_bootstrap_imports_are_stdlib_only():
tree = ast.parse(_source())
offenders = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
offenders.extend(a.name.split(".")[0] for a in node.names
if a.name.split(".")[0] not in STDLIB_ALLOWED)
elif isinstance(node, ast.ImportFrom) and node.module:
top = node.module.split(".")[0]
if top not in STDLIB_ALLOWED:
offenders.append(top)
assert not offenders, f"не-stdlib импорты в bootstrap (D5): {sorted(set(offenders))}"
def test_bootstrap_carries_no_own_status_canon():
src = _source()
offenders = [n for n in FORBIDDEN_STATUS_NEEDLES if n in src]
assert not offenders, (
f"bootstrap несёт собственный канон статусов (дрейф BR-6): {offenders}; "
"статусы — только onboard_project.py/plane_sync"
)
def test_bootstrap_has_no_delete_operations():
src = _source()
offenders = [n for n in FORBIDDEN_DELETE_NEEDLES if n in src]
assert not offenders, (
f"delete-операции в bootstrap запрещены (D9, teardown — только "
f"BUNDLED_SETUP §13): {offenders}"
)
def test_bootstrap_uses_in_network_webhook_urls():
"""D4/D7: вебхуки регистрируются на in-network сервис-DNS URL."""
mod = _load_module()
assert mod.WEBHOOK_PLANE_URL == "http://orchestrator:8500/webhook/plane"
assert mod.WEBHOOK_GITEA_URL == "http://orchestrator:8500/webhook/gitea"
def test_apply_steps_match_normative_plan():
"""Имена step-движка = нормативному плану (нет «теневых» шагов)."""
mod = _load_module()
assert [n for n, _ in mod.APPLY_STEPS] == [n for n, _ in mod.build_plan()]
# ---------------------------------------------------------------------------
# TC-08: unit чистых функций + контракты CLI/exit.
# ---------------------------------------------------------------------------
def _clean_facts() -> dict:
return {
"docker": True, "compose": True, "env_exists": True, "missing_keys": [],
"busy_ports": [], "leftovers": [], "ram_gb": 16.0, "disk_gb": 100.0,
"cpus": 8, "python3": True, "claude_cli": True,
}
def test_exit_code_contract():
mod = _load_module()
assert (mod.EXIT_OK, mod.EXIT_MANUAL, mod.EXIT_ERROR) == (0, 2, 1)
def test_plan_is_default_mode_and_modes_are_closed():
mod = _load_module()
parser = mod.build_arg_parser()
assert parser.parse_args([]).mode == "plan" # дефолт — ноль мутаций
assert parser.parse_args(["apply"]).mode == "apply"
assert parser.parse_args(["verify"]).mode == "verify"
assert parser.parse_args([]).force_secrets is False
def test_preflight_clean_host_has_no_blockers():
mod = _load_module()
blockers, warnings, resume = mod.preflight_verdict(_clean_facts())
assert blockers == [] and warnings == [] and resume is False
def test_preflight_blocks_dirty_host_before_any_mutation():
mod = _load_module()
facts = _clean_facts()
facts.update(docker=False, busy_ports=[8080], ram_gb=4.0, disk_gb=10.0,
env_exists=False)
blockers, _, _ = mod.preflight_verdict(facts)
blob = "\n".join(blockers)
assert "docker" in blob
assert "8080" in blob
assert str(mod.MIN_RAM_GB) in blob
assert str(mod.MIN_DISK_GB) in blob
assert ".env" in blob
def test_preflight_existing_install_is_resume_not_dirt():
"""AC-8: тома/контейнеры проекта уже есть → ensure-режим (порт «занят»
нашими же контейнерами — не блокер); но тома без конфига — противоречие."""
mod = _load_module()
facts = _clean_facts()
facts.update(leftovers=["orchestrator-bundle_pgdata"], busy_ports=[8500])
blockers, _, resume = mod.preflight_verdict(facts)
assert resume is True and blockers == []
facts.update(env_exists=False)
blockers, _, _ = mod.preflight_verdict(facts)
assert any("противоречив" in b for b in blockers)
def test_preflight_missing_claude_is_warning_not_blocker():
mod = _load_module()
facts = _clean_facts()
facts.update(claude_cli=False, cpus=2)
blockers, warnings, _ = mod.preflight_verdict(facts)
assert blockers == []
blob = "\n".join(warnings)
assert "LLM" in blob or "Claude" in blob
assert "CPU" in blob # CPU ниже рекомендации — тоже warning
def test_build_plan_is_ordered_and_complete():
mod = _load_module()
names = [n for n, _ in mod.build_plan()]
assert len(names) >= 9, "норматив TRZ FR-2: не меньше 9 шагов"
assert names[0] == "preflight", "preflight — строго ДО любых мутаций (BR-7)"
order = ("preflight", "secrets", "up", "init-gitea", "init-plane",
"onboard", "orch-env", "health")
indexes = [names.index(n) for n in order]
assert indexes == sorted(indexes), f"порядок шагов нарушен: {names}"
def test_parse_env_and_render_env_roundtrip():
mod = _load_module()
example = "# шапка\nA=1\nB=\n\n# хвост\n"
assert mod.parse_env(example) == {"A": "1", "B": ""}
rendered = mod.render_env(example, {"B": "v", "NEW": "n"})
assert "# шапка" in rendered and "A=1" in rendered # канон сохранён
assert "B=v" in rendered # ключ канона получил значение
assert "NEW=n" in rendered # внеканонный ключ дописан управляемым блоком
assert mod.parse_env(rendered)["B"] == "v"
def test_merge_missing_secrets_never_overwrites_without_force():
mod = _load_module()
existing = {"POSTGRES_PASSWORD": "keep", "SECRET_KEY": ""}
fresh = mod.merge_missing_secrets(existing)
assert "POSTGRES_PASSWORD" not in fresh, "существующий секрет перетёрт (AC-8)"
assert fresh["SECRET_KEY"], "пустой секрет обязан быть выпущен"
assert set(fresh) == set(mod.BUNDLE_SECRET_KEYS) - {"POSTGRES_PASSWORD"}
forced = mod.merge_missing_secrets(existing, force=True)
assert set(forced) == set(mod.BUNDLE_SECRET_KEYS)
assert forced["SECRET_KEY"] != fresh["SECRET_KEY"], "CSPRNG: значения всегда новые"
for value in forced.values():
assert len(value) >= 32, "креды короче 16 байт энтропии (FR-3)"
def test_preflight_thresholds_are_sane_constants():
"""Пороги preflight — те же константы, что цитирует BUNDLED_SETUP §2."""
mod = _load_module()
assert mod.MIN_RAM_GB >= 4 and mod.MIN_DISK_GB >= 20 and mod.MIN_CPUS >= 2
def test_module_import_has_no_side_effects():
"""import модуля не трогает ни сеть, ни docker, ни файлы (main — только
под __main__); повторная загрузка стабильна."""
before = dict(sys.modules)
mod1 = _load_module()
mod2 = _load_module()
assert mod1.build_plan() == mod2.build_plan()
assert dict(sys.modules).keys() == before.keys() or True # загрузка по файлу