from __future__ import annotations import hashlib import importlib.util import io import json import sys from dataclasses import replace from pathlib import Path from types import SimpleNamespace import pytest REPO_ROOT = Path(__file__).resolve().parents[2] CLI_PATH = REPO_ROOT / "ops" / "gitea-deploy-broker" / "repopulse_adoption_cli.py" def _load_module(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module cli = _load_module("repopulse_adoption_cli_test", CLI_PATH) def _sha(value: bytes | str) -> str: raw = value.encode("ascii") if isinstance(value, str) else value return hashlib.sha256(raw).hexdigest() def _canonical_sha(value: object) -> str: raw = json.dumps( value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False, ).encode("ascii") return hashlib.sha256(raw).hexdigest() def _projection() -> dict[str, object]: return { "repository": "jens/repopulse", "main_revision": "a" * 40, "live_revision": "b" * 40, "container_id_sha256": _sha("legacy-container"), "container_image_sha256": _sha("legacy-image"), "queue": {"queued": 0, "active": 0, "completed": 10, "failed": 0}, "database": { "database_bytes": 12345, "journal_bytes": 0, "scan_count": 10, "compacted_scan_count": 4, }, "container_state": "running", "container_health": "healthy", } def _payload() -> dict[str, object]: idempotency = _sha("itworx.repopulse-legacy-adoption/v1:jens/repopulse:cli-test") projection = _projection() return { "intentId": f"rpa-{idempotency[:20]}", "idempotencySha256": idempotency, "preflightSha256": _canonical_sha(projection), "preflightProjection": projection, "targetCredential": "A" * 64, } def _raw(payload: dict[str, object] | None = None) -> bytes: return json.dumps(payload or _payload(), separators=(",", ":")).encode("utf-8") def _core(): return cli._load_core_modules() def _parsed(payload: dict[str, object] | None = None): core = _core() return cli.parse_handoff(_raw(payload), core.executor) def test_parser_accepts_exact_handoff_and_rejects_duplicate_keys_at_any_depth(): core = _core() payload = _payload() parsed = cli.parse_handoff(_raw(payload), core.executor) assert parsed.intent_id == payload["intentId"] assert parsed.target_credential == b"A" * 64 assert parsed.preflight_projection == payload["preflightProjection"] regular = _raw(payload) duplicate_top = ( b'{"intentId":' + json.dumps(payload["intentId"]).encode("ascii") + bytes([44]) + regular[1:] ) with pytest.raises(cli.ControllerCliError) as error: cli.parse_handoff(duplicate_top, core.executor) assert error.value.code == "controller_handoff_duplicate_key" duplicate_nested = regular.replace(b'"queued":0', b'"queued":0,"queued":0', 1) with pytest.raises(cli.ControllerCliError) as error: cli.parse_handoff(duplicate_nested, core.executor) assert error.value.code == "controller_handoff_duplicate_key" @pytest.mark.parametrize( "change", [ lambda value: value.pop("targetCredential"), lambda value: value.update({"extra": True}), lambda value: value.update({"targetCredential": "short"}), lambda value: value.update({"targetCredential": "x" * 48 + "\n"}), ], ) def test_parser_rejects_schema_changes_and_noncontroller_credentials(change): core = _core() payload = _payload() change(payload) with pytest.raises(cli.ControllerCliError): cli.parse_handoff(_raw(payload), core.executor) def test_parser_enforces_bounded_stdin_size(): core = _core() with pytest.raises(cli.ControllerCliError) as error: cli.parse_handoff(b"x" * (cli._MAX_HANDOFF_BYTES + 1), core.executor) assert error.value.code == "controller_handoff_too_large" def test_reserved_binding_requires_existing_matching_intent_and_credential(): core = _core() handoff = _parsed() binding = core.store.intent_binding_sha256( handoff.intent_id, handoff.idempotency_sha256, handoff.preflight_sha256, ) record = { "intentId": handoff.intent_id, "idempotencySha256": handoff.idempotency_sha256, "preflightSha256": handoff.preflight_sha256, "intentBindingSha256": binding, "expectedTargetCredentialSha256": _sha(handoff.target_credential), "status": "in_progress", "registration": None, } class Store: def read_only_status(self): return { "status": record["status"], "hashes": { "idempotencySha256": record["idempotencySha256"], "preflightSha256": record["preflightSha256"], "intentBindingSha256": record["intentBindingSha256"], }, } def load_intent(self): return record assert cli._require_reserved_binding( store=Store(), store_module=core.store, handoff=handoff ) is record mismatched = replace(handoff, preflight_sha256=_sha("another preflight")) with pytest.raises(cli.ControllerCliError) as error: cli._require_reserved_binding( store=Store(), store_module=core.store, handoff=mismatched ) assert error.value.code == "controller_intent_binding_mismatch" wrong_credential = replace(handoff, target_credential=b"B" * 64) with pytest.raises(cli.ControllerCliError) as error: cli._require_reserved_binding( store=Store(), store_module=core.store, handoff=wrong_credential ) assert error.value.code == "target_credential_binding_mismatch" def test_missing_reservation_is_rejected_before_loading_or_creating_journal(): core = _core() handoff = _parsed() class Store: def read_only_status(self): return None def load_intent(self): raise AssertionError("missing reservation must not load mutable state") with pytest.raises(cli.ControllerCliError) as error: cli._require_reserved_binding( store=Store(), store_module=core.store, handoff=handoff ) assert error.value.code == "controller_intent_not_reserved" def test_fixed_storage_bridge_forwards_executor_revision_and_keeps_credential_in_memory(): handoff = _parsed() class StorageHooks: def __init__(self): self.calls = [] def ensure_quiesced(self, **kwargs): self.calls.append(("quiesce", kwargs)) return {"ok": True} def create_or_verify_fresh_snapshot(self, **kwargs): self.calls.append(("snapshot", kwargs)) return {"ok": True} def copy_and_verify_managed_data(self, **kwargs): self.calls.append(("copy", kwargs)) return {"ok": True} hooks = StorageHooks() bridge = cli._FixedHandoffStorageEffects( store=object(), store_module=SimpleNamespace(REPOSITORY="jens/repopulse"), storage_effects=hooks, legacy_rollback=object(), registry_restore=object(), handoff=handoff, ) revision = handoff.preflight_projection["main_revision"] binding = "c" * 64 bridge.ensure_quiesced( intent_id=handoff.intent_id, intent_binding_sha256=binding, expected_main_revision=revision, expected_container_id_sha256="d" * 64, expected_container_image_sha256="e" * 64, ) bridge.create_or_verify_fresh_snapshot( intent_id=handoff.intent_id, intent_binding_sha256=binding, expected_main_revision=revision, expected_snapshot=None, ) bridge.copy_and_verify_managed_data( intent_id=handoff.intent_id, expected_main_revision=revision, snapshot_evidence={"intentId": handoff.intent_id}, ) assert [call[0] for call in hooks.calls] == ["quiesce", "snapshot", "copy"] assert all(call[1]["expected_main_revision"] == revision for call in hooks.calls) assert bridge.registration_material(intent_id=handoff.intent_id) == ( f"enr-{handoff.intent_id[4:]}", b"A" * 64, ) with pytest.raises(cli.ControllerCliError): bridge.registration_material(intent_id="rpa-" + "f" * 20) def test_executor_store_proxy_snapshots_registry_before_registration_cas(): events = [] class Store: def register_target(self, **kwargs): events.append(("register", kwargs)) return {"status": "registered"} def read_only_status(self): return {"status": "in_progress"} class RegistryRestore: def preserve_before_registry_cas(self, store): assert isinstance(store, Store) events.append(("preserve", None)) proxy = cli._RegistrySnapshotStoreProxy(Store(), RegistryRestore()) assert proxy.register_target(intent_id="rpa-" + "a" * 20) == { "status": "registered" } assert proxy.read_only_status() == {"status": "in_progress"} assert [name for name, _value in events] == ["preserve", "register"] def test_serialized_result_rejects_secret_leaks_and_unexpected_fields(): credential = b"A" * 64 intent_id = "rpa-" + "a" * 20 result = {key: None for key in cli._RESULT_KEYS} result.update( { "apiVersion": "itworx.repopulse-controller-adoption-execution-result/v1", "repository": "jens/repopulse", "applicationId": "repopulse", "intentId": intent_id, "status": "deployment_verified", "phase": "completion_verified", "legacyVolume": "repopulse_data", "legacyVolumeRetained": True, } ) result["deploymentReceipt"] = {"note": credential.decode("ascii")} with pytest.raises(cli.ControllerCliError) as error: cli._serialize_result( result, target_credential=credential, intent_id=intent_id ) assert error.value.code == "controller_result_secret_detected" result["deploymentReceipt"] = {} result["targetCredential"] = credential.decode("ascii") with pytest.raises(cli.ControllerCliError) as error: cli._serialize_result( result, target_credential=credential, intent_id=intent_id ) assert error.value.code == "controller_result_schema_invalid" def test_disabled_controller_does_not_read_handoff_or_load_effects(): core = cli.CoreModules( store=SimpleNamespace(EXECUTION_ENABLED=False), executor=SimpleNamespace(EXECUTION_ENABLED=False), ) effects_loaded = False root_checked = False class UnreadableInput: def read(self, _size): raise AssertionError("disabled controller must not read stdin") def load_effects(_core): nonlocal effects_loaded effects_loaded = True raise AssertionError("disabled controller must not construct effects") def require_root(): nonlocal root_checked root_checked = True output = io.StringIO() code = cli._run_cli( argv=(), stdin=UnreadableInput(), stdout=output, load_core=lambda: core, load_effects=load_effects, require_root=require_root, ) assert code == 2 assert json.loads(output.getvalue()) == { "status": "disabled", "reason": "controller_adoption_execution_disabled", } assert not effects_loaded assert root_checked def test_nonroot_controller_rejects_before_loading_modules_or_reading_handoff(): core_loaded = False class UnreadableInput: def read(self, _size): raise AssertionError("nonroot controller must not read stdin") def load_core(): nonlocal core_loaded core_loaded = True raise AssertionError("nonroot controller must not load modules") def require_root(): raise cli.ControllerCliError("controller_root_required") output = io.StringIO() code = cli._run_cli( argv=(), stdin=UnreadableInput(), stdout=output, load_core=load_core, require_root=require_root, ) assert code == 2 assert json.loads(output.getvalue()) == { "status": "blocked", "reason": "controller_root_required", } assert not core_loaded def test_real_fixed_effect_modules_compose_without_side_effects(): core = cli._load_core_modules() effects = cli._load_effect_modules(core) handoff = _parsed() store = core.store.RepoPulseAdoptionStore() executor = cli._compose_executor( core=core, effects=effects, store=store, handoff=handoff, ) assert isinstance(executor, core.executor.RepoPulseAdoptionExecutor) assert executor._effects.__class__.__name__ == "_RevisionBoundRuntimeEffects"