feat(cfold): canonicalize custom test layout
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
autonomic-bot
2026-06-12 16:08:18 +00:00
parent 87928a9096
commit 44e02425ab
110 changed files with 306 additions and 241 deletions

View File

@ -72,17 +72,16 @@ def test_repo_local_wins_when_approved(tmp_path):
def test_custom_tests_repo_local_gated(tmp_path, monkeypatch):
# custom test_*.py from repo-local only count for approved recipes (HC2); placement rule
# (rcust P4): custom tests live under functional/ (or playwright/) — top-level files are
# lifecycle overlays only, so the repo-local custom here sits in functional/.
# (cfold): custom/ is canonical, while functional/ and playwright/ remain deprecated aliases.
# Use a synthetic recipe name + monkeypatched cc_ci_dir so this is independent of what
# tests/<real-recipe>/ ships (F2-1).
fake_recipe = "ccci-hc2-fixture"
monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "cc-ci" / r))
(tmp_path / "cc-ci" / fake_recipe).mkdir(parents=True)
rl = tmp_path / "repo"
(rl / "functional").mkdir(parents=True)
(rl / "functional" / "test_sso.py").write_text("# repo-local custom\n")
(rl / "functional" / "test_install.py").write_text("# lifecycle name -> excluded from custom\n")
(rl / "custom").mkdir(parents=True)
(rl / "custom" / "test_sso.py").write_text("# repo-local custom\n")
(rl / "custom" / "test_install.py").write_text("# lifecycle name -> excluded from custom\n")
_approve(tmp_path) # not approved -> repo-local custom ignored
assert discovery.custom_tests(fake_recipe, str(rl)) == []
@ -94,6 +93,25 @@ def test_custom_tests_repo_local_gated(tmp_path, monkeypatch):
assert all(os.path.basename(p) != "test_install.py" for _, p in customs)
def test_custom_tests_prefers_custom_and_warns_on_deprecated_aliases(tmp_path, monkeypatch, capsys):
fake_recipe = "ccci-cfold-fixture"
fake_dir = tmp_path / "tests" / fake_recipe
(fake_dir / "custom").mkdir(parents=True)
(fake_dir / "functional").mkdir()
(fake_dir / "playwright").mkdir()
(fake_dir / "custom" / "test_a.py").write_text("# canonical\n")
(fake_dir / "functional" / "test_b.py").write_text("# deprecated alias\n")
(fake_dir / "playwright" / "test_c.py").write_text("# deprecated alias\n")
monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "tests" / r))
customs = discovery.custom_tests(fake_recipe, None)
assert [os.path.basename(path) for _, path in customs] == ["test_a.py", "test_b.py", "test_c.py"]
err = capsys.readouterr().err
assert "deprecated folder 'functional/'" in err
assert "deprecated folder 'playwright/'" in err
def test_install_steps_repo_local_gated(tmp_path):
rl = tmp_path / "repo"
rl.mkdir()

View File

@ -1,8 +1,8 @@
"""Unit tests for Phase-2 discovery additions (plan §4.1).
Proves the `custom_tests` discovery covers exactly the per-recipe `functional/` + `playwright/`
subdirs as well as the top-level dir, while still excluding lifecycle `test_<op>.py` names and
honouring the HC2 repo-local approval gate.
Proves the `custom_tests` discovery covers exactly the per-recipe `custom/` dir, still honors the
deprecated `functional/` + `playwright/` aliases, excludes top-level lifecycle `test_<op>.py`
names, and honors the HC2 repo-local approval gate.
Run with: `cc-ci-run -m pytest tests/unit`. Located under tests/unit/ so the orchestrator never
picks these up as overlays/custom tests.
@ -27,29 +27,24 @@ def teardown_function():
os.environ.pop("CCCI_REPO_LOCAL_APPROVED_FILE", None)
def test_custom_tests_placement_rule_functional_playwright_only(tmp_path, monkeypatch):
"""Placement rule (rcust P4): custom tests are discovered ONLY under functional/ +
playwright/. A top-level non-lifecycle test_*.py is NOT discovered (top level is reserved
for lifecycle overlays); lifecycle names inside the subdirs stay excluded (defensive)."""
# Point cc-ci's per-recipe dir at a fake recipe in tmp_path
def test_custom_tests_placement_rule_custom_only(tmp_path, monkeypatch):
"""Placement rule (cfold): custom tests are discovered under custom/. A top-level
non-lifecycle test_*.py is NOT discovered (top level is reserved for lifecycle overlays);
lifecycle names inside custom/ stay excluded (defensive)."""
fake_recipe = "ccci-phase2-fixture"
fake_dir = tmp_path / "tests" / fake_recipe
(fake_dir / "functional").mkdir(parents=True)
(fake_dir / "playwright").mkdir()
(fake_dir / "custom").mkdir(parents=True)
(fake_dir / "test_sso_smoke.py").write_text("# top-level — NOT discovered since P4\n")
(fake_dir / "functional" / "test_health_check.py").write_text("# parity port\n")
(fake_dir / "functional" / "test_content_roundtrip.py").write_text("# recipe-specific\n")
(fake_dir / "playwright" / "test_login_flow.py").write_text("# UI flow\n")
# lifecycle name in functional/ should be ignored (defensive)
(fake_dir / "functional" / "test_install.py").write_text("# misfiled lifecycle name\n")
(fake_dir / "custom" / "test_health_check.py").write_text("# parity port\n")
(fake_dir / "custom" / "test_content_roundtrip.py").write_text("# recipe-specific\n")
(fake_dir / "custom" / "test_login_flow.py").write_text("# UI flow\n")
(fake_dir / "custom" / "test_install.py").write_text("# misfiled lifecycle name\n")
# Patch the cc-ci dir resolver to point at our fixture
monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "tests" / r))
customs = discovery.custom_tests(fake_recipe, None)
names = sorted((src, os.path.basename(p)) for src, p in customs)
# functional/ + playwright/ discovered; top-level custom + lifecycle name are NOT
assert ("cc-ci", "test_health_check.py") in names
assert ("cc-ci", "test_content_roundtrip.py") in names
assert ("cc-ci", "test_login_flow.py") in names
@ -58,20 +53,20 @@ def test_custom_tests_placement_rule_functional_playwright_only(tmp_path, monkey
def test_custom_tests_repo_local_subdirs_gated(tmp_path, monkeypatch):
"""HC2 gate still applies to functional/playwright subdirs under repo-local: not approved -> the
repo-local subdir contents are ignored even if they exist."""
"""HC2 gate still applies to custom/ under repo-local: not approved -> the repo-local
subdir contents are ignored even if they exist."""
fake_recipe = "ccci-phase2-fixture"
monkeypatch.setattr(discovery, "cc_ci_dir", lambda r: str(tmp_path / "cc-ci" / r))
(tmp_path / "cc-ci" / fake_recipe).mkdir(parents=True)
rl = tmp_path / "repo"
(rl / "functional").mkdir(parents=True)
(rl / "functional" / "test_repo_local_specific.py").write_text("# repo-local custom\n")
(rl / "custom").mkdir(parents=True)
(rl / "custom" / "test_repo_local_specific.py").write_text("# repo-local custom\n")
_approve(tmp_path) # empty allowlist → default-deny
_approve(tmp_path)
assert discovery.custom_tests(fake_recipe, str(rl)) == []
_approve(tmp_path, fake_recipe) # approved → repo-local subdir honored
_approve(tmp_path, fake_recipe)
customs = discovery.custom_tests(fake_recipe, str(rl))
names = {(src, os.path.basename(p)) for src, p in customs}
assert ("repo-local", "test_repo_local_specific.py") in names

View File

@ -24,13 +24,12 @@ 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, 2 functional + 1 playwright custom tests.
repo-local side: test_restore.py overlay + 1 functional custom test (visible iff approved, HC2).
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 / "functional").mkdir(parents=True)
(d / "playwright").mkdir()
(d / "custom").mkdir(parents=True)
(d / "recipe_meta.py").write_text(
"HTTP_TIMEOUT = 600\n"
"DEPS = ['keycloak']\n"
@ -41,17 +40,17 @@ def _mk_synthetic(tmp_path, monkeypatch, approved=True):
(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 / "functional" / "test_a.py").write_text("# custom\n")
(d / "functional" / "test_b.py").write_text("# custom\n")
(d / "playwright" / "test_ui.py").write_text("# custom\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 / "functional").mkdir(parents=True)
(rl / "functional" / "test_c.py").write_text("# repo-local custom\n")
(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)) # compose.ccci.yml discovery
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))
@ -61,7 +60,6 @@ def _mk_synthetic(tmp_path, monkeypatch, approved=True):
def test_manifest_complete(tmp_path, monkeypatch):
# Every surface the synthetic recipe customizes appears — nothing silently dropped (R4).
meta, rl = _mk_synthetic(tmp_path, monkeypatch)
m = manifest.build(RECIPE, meta, rl)
assert m["meta_non_default"] == {
@ -76,8 +74,8 @@ def test_manifest_complete(tmp_path, monkeypatch):
}
assert m["overlays"] == {"backup": "cc-ci", "restore": "repo-local"}
assert m["custom_tests"] == {
"cc-ci": {"functional": 2, "playwright": 1},
"repo-local": {"functional": 1},
"cc-ci": {"custom": 3},
"repo-local": {"custom": 1},
}
assert m["env_overrides"] == []
@ -87,11 +85,10 @@ def test_manifest_deterministic_and_serializable(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 # round-trips: no callables/tuples leak through
assert json.loads(json.dumps(a)) == a
def test_manifest_zero_config_floor(tmp_path, monkeypatch):
# A recipe with NO customization at all -> every section empty, render says so explicitly.
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))
@ -112,31 +109,25 @@ def test_manifest_zero_config_floor(tmp_path, monkeypatch):
def test_manifest_repo_local_hc2_gate(tmp_path, monkeypatch):
# Unapproved recipe -> repo-local overlay + custom tests INVISIBLE (same default-deny as the
# discovery they ride on; the manifest must not advertise code the run will not execute).
meta, rl = _mk_synthetic(tmp_path, monkeypatch, approved=False)
m = manifest.build(RECIPE, meta, rl)
assert m["overlays"] == {"backup": "cc-ci"} # repo-local test_restore.py gone
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") # falsy -> not an active override
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) # local dev: no CI warning
monkeypatch.setenv("DRONE", "true") # riding a CI run -> loud flag (P2c)
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):
# Meta values are repo-public by construction, but the manifest lands on the dashboard:
# secret-NAMED entries (top-level or nested dict keys, e.g. plausible's
# EXTRA_ENV["SECRET_KEY_BASE"] dummy) render as '<redacted>' — name shown, value masked.
# Non-sensitive names (incl. KEYCLOAK_* — 'KEY' matches only as a word segment) pass through.
ccci_root = tmp_path / "cc-ci-tests"
d = ccci_root / RECIPE
d.mkdir(parents=True)
@ -159,7 +150,7 @@ def test_manifest_redacts_sensitive_named_values(tmp_path, monkeypatch):
}
out = manifest.render(RECIPE, m)
assert "dummy-ci-constant" not in out and "also-dummy" not in out
assert "SECRET_KEY_BASE" in out # the key NAME stays visible
assert "SECRET_KEY_BASE" in out
def test_render_lists_every_surface(tmp_path, monkeypatch):
@ -173,5 +164,5 @@ def test_render_lists_every_surface(tmp_path, monkeypatch):
in lines
)
assert "overlays: test_backup.py(cc-ci) test_restore.py(repo-local)" in lines
assert "custom tests: functional/=2 playwright/=1 (cc-ci) functional/=1 (repo-local)" in lines
assert "custom tests: custom/=3 (cc-ci) custom/=1 (repo-local)" in lines
assert "env overrides: (none)" in lines