from __future__ import annotations import importlib.util import shutil import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] def load(name, path): spec = importlib.util.spec_from_file_location(name, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module @pytest.fixture def artifact(tmp_path, monkeypatch): renderer = load('runner_artifact_renderer', ROOT / 'scripts/prepare_controller_artifact.py') controller_dir = ROOT / 'ops/gitea-deploy-broker' monkeypatch.syspath_prepend(str(controller_dir)) controller = load('runner_artifact_controller', controller_dir / 'status_server.py') stage = tmp_path / 'controller' stage.mkdir() for name in renderer._ARTIFACT_FILES: source = ROOT / 'scripts/run_repopulse_adoption.py' if name == 'run_repopulse_adoption.py' else controller_dir / name shutil.copyfile(source, stage / name) result = subprocess.run([ sys.executable, str(ROOT / 'scripts/prepare_controller_artifact.py'), '--artifact-dir', str(stage), '--source-repository', 'jens/unraid-appops-gateway', '--source-revision', 'a' * 40, '--source-tree', 'b' * 40, '--controller-version', '2.1.13', '--built-at', '2026-10-01T12:00:00Z', ], capture_output=True, text=True, check=True) assert 'artifactSha256' in result.stdout monkeypatch.setattr(controller, 'ROOT', stage) monkeypatch.setattr(controller, 'CONTROLLER_RELEASE_PATH', stage / 'controller-release.json') return controller, stage def test_renderer_and_runtime_accept_the_same_complete_specialized_artifact(artifact): controller, _stage = artifact release = controller._controller_release() assert release['controllerVersion'] == '2.1.13' assert release['sourceRevision'] == 'a' * 40 @pytest.mark.parametrize('damage', ['missing_binding', 'missing_hub_store', 'changed_admission', 'symlink_envelope']) def test_incomplete_or_changed_specialized_artifact_cannot_be_promoted(artifact, damage): controller, stage = artifact if damage == 'missing_binding': (stage / 'runner_statistics_binding.py').unlink() elif damage == 'missing_hub_store': (stage / 'mcp_hub_release_store.py').unlink() elif damage == 'changed_admission': with (stage / 'runner_admission_policy.py').open('ab') as stream: stream.write(b'\n# changed after manifest rendering\n') else: target = stage / 'runner_resource_envelope.py' original = stage / 'moved_envelope.py' target.rename(original) try: target.symlink_to(original) except OSError: pytest.skip('Host cannot create symlink fixture') assert controller._controller_release() is None