Files
cc-ci/tests/unit/test_deps.py
autonomic-bot 472a68b32c
All checks were successful
continuous-integration/drone/push Build is passing
feat(harness): P1 — single registry-backed meta loader (rcust)
One loader: runner/harness/meta.py::load(recipe) -> RecipeMeta (frozen dataclass,
attribute access), backed by the declarative KEYS registry (14 final keys + 3
P2-deprecated). The ONLY exec() of tests/<recipe>/recipe_meta.py. Validation per
the locked decision: unknown ALL-CAPS top-level name or type mismatch = MetaError
(hard error at load); underscore-prefixed names recipe-private; callables only on
hook-typed keys.

Migrated all six legacy loaders (spec §4 L1–L6):
- run_recipe_ci.py::_load_meta deleted; orchestrator loads once, passes meta down
- tests/conftest.py::_recipe_meta deleted; meta fixture returns full RecipeMeta (R3)
- lifecycle.py::_recipe_extra_env/_recipe_meta_flag deleted; deploy_app takes meta
- deps.py::declared_deps deleted; callers read meta.DEPS
- canonical.py::is_enrolled reads through meta.load()
- screenshot.py now actually receives SCREENSHOT through the orchestrator path (R2
  fix; proven by unit test through the real load path)

Mumble private constants underscore-prefixed (_WELCOME_TEXT_MARKER/_MAX_USERS) +
importers fixed. New tests/unit/test_meta.py (all-recipes-load-clean typo gate,
MetaError cases, spec §2 baseline defaults, underscore exemption, doc sync). Docs
§4 key table now GENERATED from the registry (scripts/gen-meta-docs.py); drift
fails CI.

Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 175 passed; scripts/lint.sh -> PASS.
2026-06-10 16:46:58 +00:00

82 lines
3.5 KiB
Python

"""Unit tests for runner/harness/deps.py (Phase 2 §4.2 / Q2.3).
Pure-Python: no real deploys. Tests the declarative parts of the dep resolver — DEPS declaration
(read through the single meta loader since rcust P1), the per-dep domain derivation, and write/load
of the run state file. The deploy_deps + teardown_deps integration is exercised by real e2e against
cc-ci (Q2.4 acceptance).
"""
from __future__ import annotations
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import deps # noqa: E402
from harness import meta as meta_mod # noqa: E402
def test_declared_deps_empty_for_no_meta(monkeypatch, tmp_path):
"""A recipe with no recipe_meta.py declares no deps (rcust P1: DEPS via meta.load)."""
monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
assert meta_mod.load("ccci-no-meta").DEPS == []
def test_declared_deps_reads_DEPS_list(tmp_path, monkeypatch):
"""A recipe_meta.py with `DEPS = [...]` surfaces the list on the loaded meta (the orchestrator
reads meta.DEPS — the successor of the deleted deps.declared_deps loader)."""
recipe_dir = tmp_path / "tests" / "ccci-with-deps"
recipe_dir.mkdir(parents=True)
(recipe_dir / "recipe_meta.py").write_text('HEALTH_PATH = "/"\nDEPS = ["keycloak", "redis"]\n')
monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
assert meta_mod.load("ccci-with-deps").DEPS == ["keycloak", "redis"]
def test_dep_domain_distinct_per_dep():
"""Two deps of different kinds (same parent/pr/ref) get distinct per-run domains."""
parent = "lasuite-docs"
pr = "42"
ref = "abc123"
d1 = deps.dep_domain(parent, pr, ref, "keycloak")
d2 = deps.dep_domain(parent, pr, ref, "redis")
assert d1 != d2
# Both must look like the standard run-app pattern: <recipe[:4]>-<6hex>.ci.commoninternet.net
assert d1.endswith(".ci.commoninternet.net")
assert d2.endswith(".ci.commoninternet.net")
# The hash is determined by (parent, pr, ref, dep) — same inputs = same domain (idempotent)
assert deps.dep_domain(parent, pr, ref, "keycloak") == d1
def test_dep_domain_distinct_per_parent():
"""The same dep deployed by two different parent recipes (same dep, pr, ref) gets distinct
domains — proves the dep is parent-scoped not just dep-name-scoped."""
d1 = deps.dep_domain("lasuite-docs", "42", "abc", "keycloak")
d2 = deps.dep_domain("cryptpad", "42", "abc", "keycloak")
assert d1 != d2
def test_write_and_load_run_state(tmp_path, monkeypatch):
"""write_run_state writes JSON to $CCCI_DEPS_FILE; load_run_state reads it back."""
state_path = tmp_path / "deps.json"
monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
state = [
{"recipe": "keycloak", "domain": "kc-deadbe.ci.commoninternet.net"},
{"recipe": "redis", "domain": "redi-abc123.ci.commoninternet.net"},
]
deps.write_run_state(state)
loaded = deps.load_run_state()
assert loaded == state
def test_load_run_state_missing_env_returns_empty(monkeypatch):
"""No $CCCI_DEPS_FILE -> empty list."""
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
assert deps.load_run_state() == []
def test_write_run_state_no_env_is_noop(monkeypatch):
"""write_run_state silently no-ops without $CCCI_DEPS_FILE (so standalone helper use doesn't
require setting up the env)."""
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
deps.write_run_state([{"recipe": "x", "domain": "y"}]) # must not raise