feat(harness): P3 — uniform ctx hook convention (rcust)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
harness.meta.HookCtx (frozen): .domain, .base_url, .meta (RecipeMeta), .deps (provisioned dep creds from $CCCI_DEPS_FILE or None), .op (current lifecycle op or None); built via meta.hook_ctx() at each hook call site. All recipe callables now take ctx: EXTRA_ENV(ctx), UPGRADE_EXTRA_ENV(ctx), READY_PROBE(ctx), BACKUP_VERIFY(ctx), SCREENSHOT(page, ctx), ops.py pre_<op>(ctx). Dict-valued EXTRA_ENV/UPGRADE_EXTRA_ENV unchanged (only the callable signature moved). Call sites converted: deploy_app env shaping, perform_upgrade, wait_ready_probes (gains op=), _perform_op BACKUP_VERIFY, screenshot.capture, _run_pre_hook. Legacy signatures fail FAST with a clear migration message: the registry carries hook_params per hook key, enforced at meta.load() (MetaError names the old vs new signature); ops.py pre-op hooks get the same check at the orchestrator call site (meta.check_hook_signature) — no silent TypeError mid-run. Migrated every in-repo user mechanically (17 ops.py files; cryptpad/lasuite-*/ mailu EXTRA_ENV; mumble+lasuite-drive READY_PROBE; ghost/discourse BACKUP_VERIFY) — seeded values, probes and assertions byte-identical (domain -> ctx.domain; keycloak pre_restore's meta arg -> ctx.meta). Unit tests: hook_ctx field contract, ctx.deps from the run deps file, legacy- signature MetaError (READY_PROBE/EXTRA_ENV/SCREENSHOT + pre-op checker), ctx signatures accepted. Docs table regenerated (signature docs in key docs). Verified on cc-ci: cc-ci-run -m pytest tests/unit -q -> 180 passed; scripts/lint.sh -> PASS.
This commit is contained in:
@ -34,10 +34,12 @@ def _fake_clock(monkeypatch):
|
||||
|
||||
|
||||
# RecipeMeta (rcust P1: wait_ready_probes reads meta.READY_PROBE off the loaded object); defaults
|
||||
# + the drive-style probe hook.
|
||||
# + the drive-style probe hook (P3 ctx signature: the probe receives a HookCtx).
|
||||
_DRIVE_META = dataclasses.replace(
|
||||
harness_meta.load("ccci-no-such-recipe"),
|
||||
READY_PROBE=lambda d: [{"host": f"collabora-{d}", "path": "/hosting/discovery", "ok": (200,)}],
|
||||
READY_PROBE=lambda ctx: [
|
||||
{"host": f"collabora-{ctx.domain}", "path": "/hosting/discovery", "ok": (200,)}
|
||||
],
|
||||
)
|
||||
_NO_PROBE_META = harness_meta.load("ccci-no-such-recipe")
|
||||
|
||||
|
||||
@ -143,10 +143,11 @@ def test_underscore_names_are_private_and_exempt(tmp_path):
|
||||
def test_lowercase_helpers_ignored(tmp_path):
|
||||
r = _write_meta(
|
||||
tmp_path,
|
||||
"def _helper(d):\n return {'K': d}\n\ndef EXTRA_ENV(domain):\n return _helper(domain)\n",
|
||||
"def _helper(d):\n return {'K': d}\n\ndef EXTRA_ENV(ctx):\n return _helper(ctx.domain)\n",
|
||||
)
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert meta_mod.extra_env(meta, "x.example") == {"K": "x.example"}
|
||||
ctx = meta_mod.hook_ctx("x.example", meta)
|
||||
assert meta_mod.extra_env(meta, ctx) == {"K": "x.example"}
|
||||
|
||||
|
||||
# ---- normalization + helpers --------------------------------------------------------------------
|
||||
@ -160,13 +161,85 @@ def test_health_ok_list_normalized_to_tuple(tmp_path):
|
||||
def test_extra_env_dict_and_callable_forms(tmp_path):
|
||||
r = _write_meta(tmp_path, "EXTRA_ENV = {'A': 1}\n")
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert meta_mod.extra_env(meta, "d") == {"A": "1"} # values stringified
|
||||
assert meta_mod.extra_env(meta, meta_mod.hook_ctx("d", meta)) == {"A": "1"} # stringified
|
||||
r2 = _write_meta(
|
||||
tmp_path, "UPGRADE_EXTRA_ENV = lambda domain: {'COMPOSE_FILE': domain}\n", recipe="r2"
|
||||
tmp_path, "UPGRADE_EXTRA_ENV = lambda ctx: {'COMPOSE_FILE': ctx.domain}\n", recipe="r2"
|
||||
)
|
||||
meta2 = meta_mod.load(r2, tests_dir=str(tmp_path))
|
||||
assert meta_mod.upgrade_extra_env(meta2, "dom.x") == {"COMPOSE_FILE": "dom.x"}
|
||||
assert meta_mod.extra_env(meta2, "dom.x") == {} # unset EXTRA_ENV resolves to {}
|
||||
ctx2 = meta_mod.hook_ctx("dom.x", meta2, op="upgrade")
|
||||
assert meta_mod.upgrade_extra_env(meta2, ctx2) == {"COMPOSE_FILE": "dom.x"}
|
||||
assert meta_mod.extra_env(meta2, ctx2) == {} # unset EXTRA_ENV resolves to {}
|
||||
|
||||
|
||||
# ---- P3: uniform ctx hook convention -------------------------------------------------------------
|
||||
|
||||
|
||||
def test_hook_ctx_fields(tmp_path):
|
||||
meta = meta_mod.load("no-such", tests_dir=str(tmp_path))
|
||||
ctx = meta_mod.hook_ctx("app.ci.example", meta, op="backup")
|
||||
assert ctx.domain == "app.ci.example"
|
||||
assert ctx.base_url == "https://app.ci.example"
|
||||
assert ctx.meta is meta
|
||||
assert ctx.op == "backup"
|
||||
assert meta_mod.hook_ctx("d", meta).op is None
|
||||
|
||||
|
||||
def test_hook_ctx_deps_from_run_file(tmp_path, monkeypatch):
|
||||
import json
|
||||
|
||||
meta = meta_mod.load("no-such", tests_dir=str(tmp_path))
|
||||
monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
|
||||
assert meta_mod.hook_ctx("d", meta).deps is None
|
||||
f = tmp_path / "deps.json"
|
||||
f.write_text(json.dumps({"keycloak": {"recipe": "keycloak", "domain": "kc.x"}}))
|
||||
monkeypatch.setenv("CCCI_DEPS_FILE", str(f))
|
||||
deps = meta_mod.hook_ctx("d", meta).deps
|
||||
assert deps["keycloak"]["domain"] == "kc.x"
|
||||
f.write_text("{}") # empty dict -> None (deps declared but not provisioned)
|
||||
assert meta_mod.hook_ctx("d", meta).deps is None
|
||||
|
||||
|
||||
def test_legacy_hook_signature_raises_clear_meta_error(tmp_path):
|
||||
"""A pre-restructure hook signature must fail AT LOAD with a migration message — never a
|
||||
silent TypeError mid-run (P3.4)."""
|
||||
r = _write_meta(tmp_path, "def READY_PROBE(domain):\n return []\n")
|
||||
with pytest.raises(MetaError, match="ctx"):
|
||||
meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
r2 = _write_meta(tmp_path, "EXTRA_ENV = lambda domain: {}\n", recipe="r2")
|
||||
with pytest.raises(MetaError, match="restructure"):
|
||||
meta_mod.load(r2, tests_dir=str(tmp_path))
|
||||
r3 = _write_meta(
|
||||
tmp_path, "def SCREENSHOT(page, domain, meta):\n return None\n", recipe="r3"
|
||||
)
|
||||
with pytest.raises(MetaError, match="page, ctx"):
|
||||
meta_mod.load(r3, tests_dir=str(tmp_path))
|
||||
|
||||
|
||||
def test_ctx_hook_signatures_accepted(tmp_path):
|
||||
r = _write_meta(
|
||||
tmp_path,
|
||||
"def READY_PROBE(ctx):\n return []\n"
|
||||
"def BACKUP_VERIFY(ctx):\n return True\n"
|
||||
"def SCREENSHOT(page, ctx):\n return None\n"
|
||||
"def EXTRA_ENV(ctx):\n return {}\n",
|
||||
)
|
||||
meta = meta_mod.load(r, tests_dir=str(tmp_path))
|
||||
assert callable(meta.READY_PROBE) and callable(meta.SCREENSHOT)
|
||||
|
||||
|
||||
def test_check_hook_signature_for_pre_op_hooks():
|
||||
"""The orchestrator validates ops.py pre_<op> hooks with the same checker (legacy
|
||||
(domain, meta) form names the migration)."""
|
||||
|
||||
def legacy(domain, meta):
|
||||
pass
|
||||
|
||||
def new(ctx):
|
||||
pass
|
||||
|
||||
with pytest.raises(MetaError, match="ctx"):
|
||||
meta_mod.check_hook_signature(legacy, ("ctx",), "tests/x/ops.py::pre_upgrade")
|
||||
meta_mod.check_hook_signature(new, ("ctx",), "tests/x/ops.py::pre_upgrade") # no raise
|
||||
|
||||
|
||||
def test_non_default_reports_only_customized_keys(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user