Files
acdl/tests/test_policy_engine.py
Jon Chery ac18c98385 feat(P1): kyverno-json engine core + PolicyEngine protocol (REQ-291..294, 308, 309)
core/policy_engine.py: PolicyEngine Protocol (PEP 544, runtime_checkable)
+ PolicyEngineRegistry (selects from config.json.policy.engine) + NullEngine
fallback (NULL_ENGINE_INACTIVE when policy key absent).

adapters/kyverno-json/: KyvernoJsonEngine — shells to , translates
native output → list[dict] PCR records (engine: "kyverno", ruleId KJ_ prefix,
severity via nova.cloudinit.dev/severity annotation, default info).
is_configured() guards on  → KJ_ENGINE_NOT_CONFIGURED SKIPPED PCR
(distinct from NullEngine). Defensive parsing (malformed → error PCR).

config.json: new  object {engine: kyverno-json, policy_root}.

scripts/install-kyverno-json.sh: go install kj@latest (D-115).
CI (.gitea + .github): install Go + kj for policy-engine tests (best-effort;
tests skip when kj absent).

tests: 24 pass, 2 skip (kj not installed). 132 existing tests unchanged.
NullEngine satisfies PolicyEngine Protocol (G-Q8a — proves swap boundary).

---ci---
project: acdl
phase: 1
milestone: v1.25
status: execute
phase_role: execution
requirements:
  covered: [REQ-291, REQ-292, REQ-293, REQ-294, REQ-308, REQ-309]
  partial: []
---/ci---
2026-08-12 18:19:16 +00:00

125 lines
4.7 KiB
Python

"""Tests for core/policy_engine.py (REQ-308, v1.25).
Protocol conformance, registry selection, NullEngine fallback,
unknown-engine KeyError, and the NullEngine-satisfies-Protocol
assertion (G-Q8a — proves the swap boundary is real without
implementing OPA).
"""
import json
import os
import sys
from pathlib import Path
from unittest import mock
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import core.policy_engine as pe
class TestPolicyEngineProtocol:
def test_null_engine_satisfies_protocol(self):
# G-Q8a: NullEngine satisfies the PolicyEngine Protocol — proves
# the swap boundary is real (a second engine implements it).
eng = pe.NullEngine()
assert isinstance(eng, pe.PolicyEngine)
def test_null_engine_is_configured_false(self):
assert pe.NullEngine().is_configured() is False
def test_null_engine_evaluate_returns_skipped(self):
out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid-123")
assert len(out) == 1
pcr = out[0]
assert pcr["ruleId"] == "NULL_ENGINE_INACTIVE"
assert pcr["result"] == "skipped"
assert pcr["engine"] == "kyverno"
assert pcr["contractId"] == "cid-123"
def test_null_engine_severity_is_info(self):
out = pe.NullEngine().evaluate({}, Path("/tmp"), "cid")
assert out[0]["severity"] == "info"
class TestRegistry:
def test_register_and_get(self, tmp_path, monkeypatch):
# Register a stub engine and verify get_engine() returns it.
class StubEngine:
name = "stub"
def is_configured(self) -> bool:
return True
def evaluate(self, payload, policy_dir, contract_id):
return [{"contractId": contract_id, "engine": "kyverno",
"ruleId": "STUB", "result": "pass", "severity": "info",
"message": "", "evaluatedAt": "t", "resourceRef": "",
"evidence": {}}]
pe._REGISTRY.clear()
pe.register("stub", StubEngine)
monkeypatch.setattr(pe, "_load_config_policy", lambda: {"engine": "stub"})
eng = pe.get_engine()
assert eng.name == "stub"
pe._REGISTRY.clear()
pe._autoload_kyverno_json()
def test_unknown_engine_raises_keyerror(self, monkeypatch):
pe._REGISTRY.clear()
monkeypatch.setattr(pe, "_load_config_policy",
lambda: {"engine": "nonexistent"})
with pytest.raises(KeyError, match="Unknown policy engine"):
pe.get_engine()
pe._autoload_kyverno_json()
def test_null_engine_fallback_when_policy_key_absent(self, monkeypatch):
# G-Q4: policy key absent → NullEngine (distinct from kj-not-configured).
monkeypatch.setattr(pe, "_load_config_policy", lambda: None)
eng = pe.get_engine()
assert isinstance(eng, pe.NullEngine)
assert eng.is_configured() is False
def test_kyverno_json_registered_via_autoload(self):
# The autoload should register kyverno-json if the adapter file exists.
pe._autoload_kyverno_json()
assert "kyverno-json" in pe._REGISTRY or len(pe._REGISTRY) == 0
class TestConfigPolicyLoad:
def test_load_config_policy_returns_dict(self):
out = pe._load_config_policy()
if out is not None:
assert "engine" in out
assert out["engine"] == "kyverno-json"
def test_get_policy_root_is_path(self):
root = pe.get_policy_root()
assert isinstance(root, Path)
assert root.name == "policies" or str(root).endswith("policies")
class TestKjNotConfiguredPath:
"""G-Q4: when policy key is present but kj is absent, the engine
returns KJ_ENGINE_NOT_CONFIGURED (distinct from NullEngine's
NULL_ENGINE_INACTIVE)."""
def test_kj_not_configured_returns_distinct_ruleid(self, monkeypatch):
# Force the registry to return KyvernoJsonEngine, then mock
# `which kj` to return None.
pe._autoload_kyverno_json()
if "kyverno-json" not in pe._REGISTRY:
pytest.skip("kyverno-json adapter not loadable in this env")
monkeypatch.setattr(pe, "_load_config_policy",
lambda: {"engine": "kyverno-json"})
eng = pe.get_engine()
# Mock is_configured → False
with mock.patch.object(eng, "is_configured", return_value=False):
out = eng.evaluate({}, Path("/tmp"), "cid-456")
assert len(out) == 1
assert out[0]["ruleId"] == "KJ_ENGINE_NOT_CONFIGURED"
assert out[0]["result"] == "skipped"
assert out[0]["contractId"] == "cid-456"
# Distinct from NullEngine
assert out[0]["ruleId"] != "NULL_ENGINE_INACTIVE"