emergent-security/tests/test_determinism.py

89 lines
3.9 KiB
Python

"""Determinism and idempotency of the decision function (canon A3, G9)."""
from __future__ import annotations
import copy
import pytest
from fixture_loading import decision_cases
from emergent_security.decide import decide
from emergent_security.delegation import Delegation
from emergent_security.memory import MemoryEntry
from emergent_security.model import Request
from emergent_security.tenant import TenantContext
CASES = decision_cases()
def _decide(request_dict, context, envelope, scales, tenant, registry_order=1):
registry = [Delegation.from_dict(d) for d in context.get("registry", [])][::registry_order]
return decide(
Request.from_dict(request_dict),
envelope=envelope,
scales=scales,
registry=registry,
tenant=tenant,
memory=[MemoryEntry.from_dict(m) for m in context.get("memory", [])],
)
@pytest.mark.parametrize(("request_dict", "context", "expect"), CASES)
def test_same_inputs_same_decision(request_dict, context, expect, default_envelope, scales, tenant_context_from):
tenant = tenant_context_from(context)
first = _decide(request_dict, context, default_envelope, scales, tenant)
second = _decide(request_dict, context, default_envelope, scales, tenant)
assert first == second
@pytest.mark.parametrize(("request_dict", "context", "expect"), CASES)
def test_inputs_are_not_mutated(request_dict, context, expect, default_envelope, scales, tenant_context_from):
snapshot = copy.deepcopy((request_dict, context))
_decide(request_dict, context, default_envelope, scales, tenant_context_from(context))
assert (request_dict, context) == snapshot
@pytest.mark.parametrize(("request_dict", "context", "expect"), CASES)
def test_registry_order_does_not_matter(request_dict, context, expect, default_envelope, scales, tenant_context_from):
tenant = tenant_context_from(context)
forward = _decide(request_dict, context, default_envelope, scales, tenant, registry_order=1)
backward = _decide(request_dict, context, default_envelope, scales, tenant, registry_order=-1)
assert forward == backward
def test_most_specific_registry_delegation_wins(default_envelope, scales):
request = {
"request_id": "r", "principal": "agent://a", "decision_class": "git.merge_protected",
"scope": {"level": "L0", "temperature": "hot", "phase": "build", "tenant": "none",
"repository": "factory/billing", "environment": "dev"},
"risk": {}, "attributes": {}, "evidence": [],
}
broad = {"id": "delegation://broad", "decision_class": "git.merge_protected", "scope": {},
"mode": "autonomous", "risk_envelope": {}}
narrow = {"id": "delegation://narrow", "decision_class": "git.merge_protected",
"scope": {"repository": "factory/billing"}, "mode": "autonomous", "risk_envelope": {}}
for order in ([broad, narrow], [narrow, broad]):
decision = _decide(request, {"registry": order}, default_envelope, scales, None)
assert decision.source == "delegation://narrow"
@pytest.mark.parametrize(
("request_dict", "context", "expect"),
[c for c in CASES if "tenant_governance" not in c.values[1] and "platform_envelope" in c.values[1]],
)
def test_auto_mode_equals_no_tenant_configuration(request_dict, context, expect, default_envelope, scales, tenant_context_from):
"""G7: `auto` mode yields the same decision as having no tenant governance at all."""
from emergent_security.tenant import TenantGovernance
without = tenant_context_from(context)
auto = TenantContext(
platform=without.platform,
governance=TenantGovernance.from_dict(
{"tenant": without.platform.tenant, "mode": "auto", "bounded_by": without.platform.id,
"established_by": "human://acme/admin-1", "on_deadline": "escalate_to_next_admin"}
),
)
assert _decide(request_dict, context, default_envelope, scales, without) == _decide(
request_dict, context, default_envelope, scales, auto
)