import importlib.util import sys from pathlib import Path import yaml TOOL_PATH = Path(__file__).resolve().parents[1] / "playbook_contract_validator.py" SPEC = importlib.util.spec_from_file_location("playbook_contract_validator", TOOL_PATH) validator = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None sys.modules[SPEC.name] = validator SPEC.loader.exec_module(validator) def valid_declaration() -> dict: return { "apiVersion": validator.API_VERSION, "kind": validator.KIND, "metadata": { "id": "railiance-infra.bootstrap-host", "name": "Railiance S1 host bootstrap", "owner": "railiance-infra", "repo": "railiance-infra", "domain": "railiance", "contract_version": "0.1", }, "spec": { "playbook": { "path": "ansible/playbooks/bootstrap.yaml", "type": "ansible", "invocation": "make converge", "description": "Converges the S1 host baseline.", }, "capabilities": [ { "id": "s1.os-baseline", "tier": "S1", "resource_kinds": ["infrastructure_resources", "secrets_credentials"], "description": "OS baseline and bootstrap secret handling.", } ], "parameters": [ { "name": "target_hosts", "type": "array", "required": True, "constraints": {"min_items": 1}, "sensitivity": "operational", "tuning_authority": "netkingdom_tunable", "description": "Inventory hosts to converge.", }, { "name": "swapfile_size_mb", "type": "integer", "required": False, "default": 4096, "constraints": {"minimum": 0, "maximum": 65536}, "sensitivity": "operational", "tuning_authority": "netkingdom_tunable", "description": "Swap file size.", }, { "name": "wireguard_enabled", "type": "boolean", "required": False, "default": False, "sensitivity": "security_sensitive", "tuning_authority": "platform_only", "description": "Enable WireGuard role.", }, ], "responsibilities": [ { "resource_kind": "infrastructure_resources", "owner": "railiance-infra", "resources": ["server:target_hosts", "os-baseline"], "repo_owns": "Ansible convergence mechanics.", "netkingdom_orchestrates": "Whether S1 is selected for the scenario.", } ], "trust": { "requires": [], "satisfies": [ { "state": "bare_host_trust", "readiness_checks": [ { "id": "os-baseline-converged", "description": "Ansible baseline converged.", "evidence": "bootstrap playbook completed", } ], } ], }, "catalog": { "publish": "capabilities/playbooks/railiance-infra.bootstrap-host.yaml", "maturity": "reference", "consumers": ["netkingdom-meta-orchestration"], }, }, } def declaration_from(data: dict, tmp_path: Path) -> validator.Declaration: path = tmp_path / "declaration.yaml" path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") return validator.Declaration(path=path, data=data) def error_messages(issues): return [item.message for item in issues if item.level == "ERROR"] def test_valid_declaration_passes(tmp_path): declaration = declaration_from(valid_declaration(), tmp_path) issues = validator.validate_declaration(declaration) assert error_messages(issues) == [] def test_unknown_capability_fails(tmp_path): data = valid_declaration() data["spec"]["capabilities"][0]["id"] = "s9.magic" declaration = declaration_from(data, tmp_path) issues = validator.validate_declaration(declaration) assert any("unknown capability id" in msg for msg in error_messages(issues)) def test_tenant_tunable_secret_reference_fails(tmp_path): data = valid_declaration() data["spec"]["parameters"][2]["sensitivity"] = "secret_reference" data["spec"]["parameters"][2]["tuning_authority"] = "tenant_tunable" declaration = declaration_from(data, tmp_path) issues = validator.validate_declaration(declaration) assert any("security-sensitive parameters cannot be tenant_tunable" in msg for msg in error_messages(issues)) def test_scenario_composition_selects_and_overrides(tmp_path): declaration = declaration_from(valid_declaration(), tmp_path) scenario = { "id": "scenario:s1-host-bootstrap-reference", "authority": "platform", "requires": {"capabilities": ["s1.os-baseline"]}, "parameter_overrides": { "railiance-infra.bootstrap-host": { "target_hosts": ["railiance01"], "swapfile_size_mb": 8192, } }, } issues, composed = validator.compose_scenario([declaration], scenario) assert error_messages(issues) == [] selected = composed["selected_declarations"][0] assert selected["id"] == "railiance-infra.bootstrap-host" assert selected["parameters"]["target_hosts"]["source"] == "override" assert selected["parameters"]["swapfile_size_mb"]["value"] == 8192 def test_tenant_authority_cannot_override_platform_only(tmp_path): declaration = declaration_from(valid_declaration(), tmp_path) scenario = { "id": "scenario:bad-tenant-override", "authority": "tenant", "requires": {"capabilities": ["s1.os-baseline"]}, "parameter_overrides": { "railiance-infra.bootstrap-host": { "target_hosts": ["tenant-host"], "wireguard_enabled": True, } }, } issues, _ = validator.compose_scenario([declaration], scenario) assert any("tenant authority cannot override platform_only" in msg for msg in error_messages(issues)) def test_required_parameter_without_override_fails(tmp_path): declaration = declaration_from(valid_declaration(), tmp_path) scenario = { "id": "scenario:missing-required-parameter", "authority": "platform", "requires": {"capabilities": ["s1.os-baseline"]}, "parameter_overrides": {}, } issues, _ = validator.compose_scenario([declaration], scenario) assert any("required parameter has no default or override" in msg for msg in error_messages(issues))