"""Focused HUX-12 immutable release-ledger tests.""" from __future__ import annotations import json import sys from pathlib import Path from types import SimpleNamespace import pytest ROOT = Path(__file__).resolve().parents[2] FOUNDATION = ROOT / "dockerfiles" / "hermes-hux-foundation" if str(FOUNDATION) not in sys.path: sys.path.insert(0, str(FOUNDATION)) from hux import contracts, identity, releases, store # noqa: E402 from hux.errors import Invalid # noqa: E402 from hux.server import build_router # noqa: E402 HEADERS = {"X-Hermes-Tenant-Identity": "slot-3", "X-Hux-Subject": "usr_0123456789abcdef", "X-Hux-Surface": "chat", "X-Hux-Relay-Key": "rk"} EVIDENCE = {**HEADERS, "X-Hux-Surface": "api", "X-Hux-Trust": "evidence", "X-Hux-Relay-Key": "evidence-key-value-that-is-at-least-32-bytes"} OTHER = {**HEADERS, "X-Hux-Subject": "usr_fedcba9876543210"} ALL_ON = ",".join(card["flag"] for card in contracts.load_flags()["cards"]) DIGEST = "sha256:" + "a" * 64 OTHER_DIGEST = "sha256:" + "b" * 64 SCHEMAS = contracts.load_all() SCHEMAS["release-ledger.schema.json"] = contracts.load_schema("release-ledger.schema.json") def call(router, method, path, body=None, headers=None): raw = b"" if body is None else json.dumps(body).encode() response = router.dispatch(method, path, {**HEADERS, **(headers or {})}, raw) return response.status, response.body, response.headers def tenant(router): return store.TenantStore(router.data_root, identity.resolve(HEADERS, {"HUX_ROUTER_KEY": "rk"})) @pytest.fixture def setup(tmp_path): key_file = tmp_path / "evidence.key" key_file.write_text("evidence-key-value-that-is-at-least-32-bytes\n") key_file.chmod(0o400) policy_file = tmp_path / "evidence-policy.json" policy_file.write_text(json.dumps({ "schema": "hux.release_evidence_policy.v1", "max_evidence_age_seconds": 900, "workloads": {"hermes-webui": { "review_url_prefix": "https://git.bstein.dev/titan/atlas-iac/pulls/", "jenkins_job_url": "https://jenkins.bstein.dev/job/hermes", "image_repository": "registry.bstein.dev/bstein/hermes", "flux_kustomization": "hermes", "health_url": "https://chat.bstein.dev/healthz", }}, })) policy_file.chmod(0o444) router = build_router(tmp_path, { "HUX_FLAGS": ALL_ON, "HUX_ROUTER_KEY": "rk", "HUX_RELEASE_EVIDENCE_KEY_FILE": str(key_file), "HUX_RELEASE_EVIDENCE_POLICY_FILE": str(policy_file), }) _, project, _ = call(router, "POST", "/hux/v1/projects", {"name": "P"}) _, conversation, _ = call(router, "POST", "/hux/v1/conversations", {"title": "C", "project_id": project["id"]}) base = f"/hux/v1/projects/{project['id']}/conversations/{conversation['id']}/releases" return router, project, conversation, base def create(setup, key="release-key-001", commit="1" * 40, **changes): router, _, _, base = setup body = { "workload": "hermes-webui", "commit": commit, "feature_flags": ["hux.foundation"], "evidence": {"review_url": "https://git.bstein.dev/titan/atlas-iac/pulls/55"}, **changes, } return call(router, "POST", base, body, {"If-Match": "0", "Idempotency-Key": key}) def move(router, path, revision, key, target, evidence): return call(router, "POST", path + "/transitions", {"to": target, "evidence": evidence}, { **EVIDENCE, "If-Match": str(revision), "Idempotency-Key": key, }) def build_evidence(digest=DIGEST): return { "ci_build_url": "https://jenkins.bstein.dev/job/hermes/20/", "image_ref": f"registry.bstein.dev/bstein/hermes:build-20@{digest}", "image_digest": digest, "harbor_digest": digest, } def through_built(setup, key_suffix="a", commit="2" * 40): router, _, _, base = setup created = create(setup, f"create-built-{key_suffix}", commit)[1] path = base + "/" + created["release"]["id"] move(router, path, 1, f"merge-built-{key_suffix}", "merged", {"merge_commit": "3" * 40}) built = move(router, path, 2, f"build-built-{key_suffix}", "built", build_evidence())[1] return path, built def test_full_release_chain_is_exact_immutable_and_live_only_after_convergence(setup): router, _, _, base = setup status, created, headers = create(setup) assert (status, created["revision"], headers["ETag"]) == (201, 1, "1") assert contracts.validate("release.schema.json", created["release"]) == [] release_id = created["release"]["id"] path = base + "/" + release_id steps = [ ("merged", {"merge_commit": "2" * 40}), ("built", build_evidence()), ("verified", {}), ("deployed", {"flux_revision": "main@sha1:" + "3" * 40}), ("converged", {"pod_digest": DIGEST}), ("live_verified", {"health_check": {"url": "/healthz", "status": "pass", "at": "2026-08-24T12:00:00Z"}}), ] view = created for revision, (target, evidence) in enumerate(steps, 1): status, view, headers = move(router, path, revision, f"transition-{target}", target, evidence) assert status == 200 and view["release"]["state"] == target assert view["revision"] == revision + 1 and headers["ETag"] == str(revision + 1) assert contracts.validate("release.schema.json", view["release"]) == [] assert view["release"]["evidence"]["image_digest"] == view["release"]["evidence"]["harbor_digest"] == view["release"]["evidence"]["pod_digest"] rows = tenant(router).read(releases.FAMILY, release_id) assert len(rows) == 7 and [row["sequence"] for row in rows] == list(range(1, 8)) assert all(contracts.validate("release-ledger.schema.json", row, SCHEMAS) == [] for row in rows) assert all(row["previous_hash"] == (releases.ZERO_HASH if index == 0 else rows[index - 1]["entry_hash"]) for index, row in enumerate(rows)) assert call(router, "GET", path)[1] == view listed = call(router, "GET", base)[1] assert listed["items"] == [view] def test_create_replay_uniqueness_and_scope(setup): router, project, _, base = setup status, created, _ = create(setup) status, replay, headers = create(setup) assert status == 200 and replay == created and headers["HUX-Replayed"] == "true" assert create(setup, "release-key-001", "2" * 40)[0] == 409 assert create(setup, "release-key-002")[0] == 409 assert create(setup, "release-key-003", "2" * 40)[0] == 201 assert call(router, "GET", base + "/" + created["release"]["id"], headers=OTHER)[0] == 404 wrong = base.replace(project["id"], "prj_missing0000") assert call(router, "GET", wrong + "/" + created["release"]["id"])[0] == 404 assert call(router, "GET", base + "/rel_missing0000")[0] == 404 assert call(router, "GET", base + "/BAD")[0] == 404 @pytest.mark.parametrize("changes", [ {"workload": "unknown"}, {"commit": "short"}, {"feature_flags": "hux.foundation"}, {"feature_flags": ["bad flag"]}, {"evidence": {}}, {"evidence": {"review_url": "http://git.bstein.dev/pr/1"}}, {"evidence": {"review_url": "https://user:pass@git.bstein.dev/pr/1"}}, {"evidence": {"review_url": "https://git.bstein.dev/pr/1?token=secret"}}, ]) def test_create_validation_is_fail_closed(setup, changes): commit = changes.get("commit", "4" * 40) rest = {key: value for key, value in changes.items() if key != "commit"} status, error, _ = create(setup, f"bad-release-{abs(hash(repr(changes)))}", commit, **rest) assert status == 400 and error["code"] == "invalid" def test_create_requires_headers_body_and_capacity(setup, monkeypatch): router, _, _, base = setup body = {"workload": "hermes-webui", "commit": "1" * 40, "evidence": {"review_url": "https://git.bstein.dev/pr/1"}} assert call(router, "POST", base, body, {"Idempotency-Key": "no-if-match"})[0] == 400 assert call(router, "POST", base, body, {"If-Match": "0"})[0] == 400 assert router.dispatch("POST", base, HEADERS, b"[]").status == 400 assert call(router, "POST", base, {**body, "extra": True}, {"If-Match": "0", "Idempotency-Key": "extra-field-key"})[0] == 400 monkeypatch.setattr(releases, "MAX_RELEASES", 0) assert call(router, "POST", base, body, {"If-Match": "0", "Idempotency-Key": "over-release-limit"})[0] == 413 def test_transition_requires_exact_order_revision_and_evidence(setup): router, _, _, base = setup created = create(setup)[1] path = base + "/" + created["release"]["id"] assert move(router, path, 1, "skip-transition", "built", build_evidence())[0] == 409 assert move(router, path, 1, "rollback-too-soon", "rolled_back", {"rollback_target": OTHER_DIGEST})[0] == 409 assert move(router, path, 2, "stale-transition", "merged", {"merge_commit": "2" * 40})[0] == 409 for index, evidence in enumerate(({}, {"merge_commit": "short"}, {"merge_commit": "2" * 40, "extra": "x"})): assert move(router, path, 1, f"bad-merge-{index}", "merged", evidence)[0] == 400 status, merged, _ = move(router, path, 1, "good-merge-key", "merged", {"merge_commit": "2" * 40}) assert status == 200 and merged["revision"] == 2 status, replay, headers = move(router, path, 1, "good-merge-key", "merged", {"merge_commit": "2" * 40}) assert status == 200 and replay == merged and headers["HUX-Replayed"] == "true" assert move(router, path, 2, "good-merge-key", "built", build_evidence())[0] == 409 assert call(router, "POST", path + "/transitions", {"to": "built", "evidence": build_evidence()}, { **EVIDENCE, "Idempotency-Key": "missing-transition-if", })[0] == 400 def test_only_router_reviews_and_only_evidence_trust_transitions(setup): router, _, _, base = setup created = create(setup, "trust-bound-create")[1] path = base + "/" + created["release"]["id"] transition = {"to": "merged", "evidence": {"merge_commit": "2" * 40}} headers = {"If-Match": "1", "Idempotency-Key": "trust-bound-transition"} assert call(router, "POST", path + "/transitions", transition, headers)[0] == 403 worker = {**HEADERS, "X-Hux-Surface": "worker", "X-Hux-Trust": "worker", "X-Hux-Relay-Key": "wk"} assert call(router, "POST", path + "/transitions", transition, {**worker, **headers})[0] == 401 assert call(router, "POST", base, { "workload": "hermes-webui", "commit": "8" * 40, "evidence": {"review_url": "https://git.bstein.dev/titan/atlas-iac/pulls/8"}, }, {**EVIDENCE, "If-Match": "0", "Idempotency-Key": "evidence-cannot-review"})[0] == 403 @pytest.mark.parametrize("evidence", [ {**build_evidence(), "harbor_digest": OTHER_DIGEST}, {**build_evidence(), "image_digest": OTHER_DIGEST}, {**build_evidence(), "image_ref": "registry.bstein.dev/bstein/hermes:tag@" + OTHER_DIGEST}, {**build_evidence(), "ci_build_url": "http://jenkins.bstein.dev/job/1"}, {**build_evidence(), "ci_build_url": "https://jenkins.bstein.dev/job/1?token=x"}, {"ci_build_url": "https://jenkins.bstein.dev/job/1", "image_ref": "bad", "image_digest": DIGEST, "harbor_digest": DIGEST}, ]) def test_build_requires_matching_image_and_harbor_evidence(setup, evidence): router, _, _, base = setup suffix = hashlib_key(evidence) created = create(setup, f"create-{suffix}", (suffix * 4)[:40])[1] path = base + "/" + created["release"]["id"] assert move(router, path, 1, f"merge-{suffix}", "merged", {"merge_commit": "5" * 40})[0] == 200 assert move(router, path, 2, f"build-{suffix}", "built", evidence)[0] == 400 def hashlib_key(value): import hashlib return hashlib.sha256(repr(value).encode()).hexdigest()[:12] def test_deploy_convergence_health_and_rollback_checks(setup): router, _, _, _ = setup path, built = through_built(setup) assert built["release"]["state"] == "built" assert move(router, path, 3, "deploy-before-verify", "deployed", {"flux_revision": "main@sha1:" + "3" * 40})[0] == 409 assert move(router, path, 3, "verify-with-data", "verified", {"harbor_digest": DIGEST})[0] == 400 assert move(router, path, 3, "verify-ok", "verified", {})[0] == 200 assert move(router, path, 4, "deploy-bad", "deployed", {"flux_revision": "feature@sha1:" + "3" * 40})[0] == 400 assert move(router, path, 4, "deploy-ok", "deployed", {"flux_revision": "main@sha1:" + "3" * 40})[0] == 200 assert move(router, path, 5, "converge-bad", "converged", {"pod_digest": OTHER_DIGEST})[0] == 400 assert move(router, path, 5, "converge-ok", "converged", {"pod_digest": DIGEST})[0] == 200 bad_health = [ {"health_check": {"url": "/healthz", "status": "fail", "at": "2026-08-24T12:00:00Z"}}, {"health_check": {"url": "http://public.example/health", "status": "pass", "at": "2026-08-24T12:00:00Z"}}, {"health_check": {"url": "/healthz?token=x", "status": "pass", "at": "2026-08-24T12:00:00Z"}}, ] for index, evidence in enumerate(bad_health): assert move(router, path, 6, f"health-bad-{index}", "live_verified", evidence)[0] == 400 assert move(router, path, 6, "health-ok", "live_verified", {"health_check": {"url": "https://chat.bstein.dev/healthz", "status": "pass", "at": "2026-08-24T12:00:00Z"}})[0] == 200 assert move(router, path, 7, "after-live", "live_verified", {})[0] == 409 def test_rollback_is_terminal_and_digest_bound(setup): router, _, _, _ = setup path, _ = through_built(setup, "rollback", "6" * 40) assert move(router, path, 3, "rollback-bad-digest", "rolled_back", {"rollback_target": "bad"})[0] == 400 status, rolled, _ = move(router, path, 3, "rollback-ok", "rolled_back", {"rollback_target": OTHER_DIGEST}) assert status == 200 and rolled["release"]["state"] == "rolled_back" assert move(router, path, 4, "rollback-terminal", "verified", {})[0] == 409 def test_tamper_breaks_reads_and_idempotency_fails_closed(setup): router, _, _, base = setup created = create(setup)[1] release_id = created["release"]["id"] ledger = tenant(router).root / releases.FAMILY / f"{release_id}.jsonl" rows = ledger.read_text().splitlines() row = json.loads(rows[0]) row["snapshot"]["state"] = "live_verified" ledger.write_text(json.dumps(row) + "\n") status, error, _ = call(router, "GET", base + "/" + release_id) assert status == 400 and "integrity" in error["message"] status, error, _ = create(setup) assert status == 400 and "integrity" in error["message"] def test_inconsistent_idempotency_and_cross_conversation_views_fail_closed(setup): router, project, _, base = setup created = create(setup)[1] idem = tenant(router).read(releases.FAMILY, "idempotency") idem[0]["sequence"] = 99 tenant(router).rewrite(releases.FAMILY, "idempotency", idem) status, error, _ = create(setup) assert status == 400 and "idempotency ledger" in error["message"] _, conversation2, _ = call(router, "POST", "/hux/v1/conversations", {"title": "C2", "project_id": project["id"]}) base2 = f"/hux/v1/projects/{project['id']}/conversations/{conversation2['id']}/releases" body = {"workload": "hermes-agent", "commit": "9" * 40, "evidence": {"review_url": "https://git.bstein.dev/pr/99"}} second = call(router, "POST", base2, body, {"If-Match": "0", "Idempotency-Key": "second-scope-key"})[1] first_id = created["release"]["id"] second_id = second["release"]["id"] assert call(router, "GET", base2 + "/" + first_id)[0] == 404 assert call(router, "GET", base + "/" + second_id)[0] == 404 assert move(router, base2 + "/" + first_id, 1, "cross-transition", "merged", {"merge_commit": "8" * 40})[0] == 404 listed = call(router, "GET", base)[1] assert [item["release"]["id"] for item in listed["items"]] == [first_id] def test_internal_helpers_are_bounded_and_worker_actor_has_no_subject(): worker = SimpleNamespace(identity=identity.Identity("slot-3", "usr_0123456789abcdef", "worker", "worker")) assert releases._actor(worker) == {"type": "system", "id": "release-worker"} producer = SimpleNamespace(identity=identity.Identity("slot-3", "usr_0123456789abcdef", "api", "evidence")) assert releases._actor(producer) == {"type": "system", "id": "release-evidence-producer"} with pytest.raises(Invalid, match="missing or too long"): releases._safe_url(None) with pytest.raises(Invalid, match="secret"): releases._safe_url("https://git.bstein.dev/ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") with pytest.raises(Invalid): releases._safe_url("https://git.bstein.dev/pr/1#fragment") with pytest.raises(Invalid, match="ledger entry"): releases._validate_entry({"schema": "broken"}) with pytest.raises(Invalid, match="does not match Harbor"): releases._transition_evidence({"evidence": {"image_digest": DIGEST, "harbor_digest": OTHER_DIGEST}}, "verified", {}) for path in (FOUNDATION / "hux" / "releases.py", ROOT / "services" / "hermes" / "contracts" / "hux" / "release-ledger.schema.json", Path(__file__)): assert len(path.read_text().splitlines()) <= 500