"""Unit tests for the customization manifest (rcust P5; spec §8 R4 mitigation). The manifest is PURE PRESENTATION (must never influence a verdict); these tests pin that it is COMPLETE (every customization surface a synthetic recipe exercises shows up), DETERMINISTIC (same inputs -> byte-identical JSON), serializable, and HC2-honoring (unapproved repo-local contributions are invisible). Pure / tmp-file only. Run cold: cc-ci-run -m pytest tests/unit/test_manifest.py -q """ from __future__ import annotations import json import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner")) from harness import discovery, manifest # noqa: E402 from harness import meta as meta_mod # noqa: E402 RECIPE = "ccci-manifest-fixture" def _mk_synthetic(tmp_path, monkeypatch, approved=True): """A synthetic recipe dir exercising EVERY manifest surface, plus a repo-local tests dir. cc-ci side: meta (2 data keys + 1 hook key non-default), ops.py (2 pre-ops), install_steps.sh, compose.ccci.yml, test_backup.py overlay, 3 custom tests. repo-local side: test_restore.py overlay + 1 custom test (visible iff approved, HC2). """ ccci_root = tmp_path / "cc-ci-tests" d = ccci_root / RECIPE (d / "custom").mkdir(parents=True) (d / "recipe_meta.py").write_text( "HTTP_TIMEOUT = 600\n" "DEPS = ['keycloak']\n" "def EXTRA_ENV(ctx):\n return {}\n" "_PRIVATE = 'exempt'\n" ) (d / "ops.py").write_text("def pre_upgrade(ctx):\n pass\n\ndef pre_backup(ctx):\n pass\n") (d / "install_steps.sh").write_text("#!/usr/bin/env bash\n") (d / "compose.ccci.yml").write_text("version: '3.8'\n") (d / "test_backup.py").write_text("# lifecycle overlay\n") (d / "custom" / "test_a.py").write_text("# custom\n") (d / "custom" / "test_b.py").write_text("# custom\n") (d / "custom" / "test_ui.py").write_text("# custom\n") rl = tmp_path / "repo-local" (rl / "custom").mkdir(parents=True) (rl / "custom" / "test_c.py").write_text("# repo-local custom\n") (rl / "test_restore.py").write_text("# repo-local lifecycle overlay\n") monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r)) monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root)) approved_file = tmp_path / "approved.txt" approved_file.write_text(f"{RECIPE}\n" if approved else "") monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(approved_file)) meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root)) return meta, str(rl) def test_manifest_complete(tmp_path, monkeypatch): meta, rl = _mk_synthetic(tmp_path, monkeypatch) m = manifest.build(RECIPE, meta, rl) assert m["meta_non_default"] == { "DEPS": ["keycloak"], "EXTRA_ENV": "", "HTTP_TIMEOUT": 600, } assert m["hooks"] == { "ops.py": {"cc-ci": ["pre_backup", "pre_upgrade"]}, "install_steps.sh": "cc-ci", "compose.ccci.yml": "cc-ci", } assert m["overlays"] == {"backup": "cc-ci", "restore": "repo-local"} assert m["custom_tests"] == { "cc-ci": {"custom": 3}, "repo-local": {"custom": 1}, } assert m["env_overrides"] == [] def test_manifest_deterministic_and_serializable(tmp_path, monkeypatch): meta, rl = _mk_synthetic(tmp_path, monkeypatch) a = manifest.build(RECIPE, meta, rl) b = manifest.build(RECIPE, meta, rl) assert json.dumps(a, sort_keys=True) == json.dumps(b, sort_keys=True) assert json.loads(json.dumps(a)) == a def test_manifest_zero_config_floor(tmp_path, monkeypatch): ccci_root = tmp_path / "cc-ci-tests" (ccci_root / RECIPE).mkdir(parents=True) monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r)) monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root)) monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(tmp_path / "missing.txt")) meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root)) m = manifest.build(RECIPE, meta, None) assert m == { "meta_non_default": {}, "hooks": {}, "overlays": {}, "custom_tests": {}, "env_overrides": [], } out = manifest.render(RECIPE, m) assert f"===== customization manifest: {RECIPE} =====" in out assert "(none — zero-config floor)" in out def test_manifest_repo_local_hc2_gate(tmp_path, monkeypatch): meta, rl = _mk_synthetic(tmp_path, monkeypatch, approved=False) m = manifest.build(RECIPE, meta, rl) assert m["overlays"] == {"backup": "cc-ci"} assert "repo-local" not in m["custom_tests"] def test_manifest_env_overrides_and_ci_flag(tmp_path, monkeypatch): meta, rl = _mk_synthetic(tmp_path, monkeypatch) monkeypatch.setenv("CCCI_SKIP_GENERIC_BACKUP", "1") monkeypatch.setenv("CCCI_SKIP_GENERIC_UPGRADE", "0") m = manifest.build(RECIPE, meta, rl) assert m["env_overrides"] == ["CCCI_SKIP_GENERIC_BACKUP"] monkeypatch.delenv("DRONE", raising=False) assert "!!" not in manifest.render(RECIPE, m) monkeypatch.setenv("DRONE", "true") assert "!! dev-only override active in CI" in manifest.render(RECIPE, m) def test_manifest_redacts_sensitive_named_values(tmp_path, monkeypatch): ccci_root = tmp_path / "cc-ci-tests" d = ccci_root / RECIPE d.mkdir(parents=True) (d / "recipe_meta.py").write_text( "EXTRA_ENV = {\n" " 'SECRET_KEY_BASE': 'dummy-ci-constant',\n" " 'API_KEY': 'also-dummy',\n" " 'KEYCLOAK_URL': 'https://kc.example',\n" "}\n" ) monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(ccci_root / r)) monkeypatch.setattr(meta_mod, "TESTS_DIR", str(ccci_root)) monkeypatch.setenv("CCCI_REPO_LOCAL_APPROVED_FILE", str(tmp_path / "missing.txt")) meta = meta_mod.load(RECIPE, tests_dir=str(ccci_root)) m = manifest.build(RECIPE, meta, None) assert m["meta_non_default"]["EXTRA_ENV"] == { "SECRET_KEY_BASE": "", "API_KEY": "", "KEYCLOAK_URL": "https://kc.example", } out = manifest.render(RECIPE, m) assert "dummy-ci-constant" not in out and "also-dummy" not in out assert "SECRET_KEY_BASE" in out def test_render_lists_every_surface(tmp_path, monkeypatch): meta, rl = _mk_synthetic(tmp_path, monkeypatch) out = manifest.render(RECIPE, manifest.build(RECIPE, meta, rl)) lines = out.splitlines() assert lines[0] == f"===== customization manifest: {RECIPE} =====" assert "meta (non-default): DEPS=['keycloak'] EXTRA_ENV='' HTTP_TIMEOUT=600" in lines assert ( "hooks: ops.py[pre_backup,pre_upgrade](cc-ci) install_steps.sh(cc-ci) compose.ccci.yml(cc-ci)" in lines ) assert "overlays: test_backup.py(cc-ci) test_restore.py(repo-local)" in lines assert "custom tests: custom/=3 (cc-ci) custom/=1 (repo-local)" in lines assert "env overrides: (none)" in lines