Skip to content

Commit ab1f63e

Browse files
committed
test: harden eu ai act generated compliance contracts
1 parent abc037e commit ab1f63e

1 file changed

Lines changed: 136 additions & 1 deletion

File tree

tests/compliance/test_eu_ai_act_generated.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ class EUAIControl:
3535
tool_name: str
3636

3737

38+
@dataclass(frozen=True)
39+
class EUAIControlMappingContract:
40+
article: str
41+
modeled_sink_type: str
42+
modeled_reason_code: str
43+
scope_note: str
44+
45+
3846
EUAI_CONTROLS: tuple[EUAIControl, ...] = (
3947
EUAIControl("Article 9", "Risk Management", "shell.exec", "bash_execute"),
4048
EUAIControl("Article 10", "Data Governance", "filesystem.read", "read_file"),
@@ -46,6 +54,57 @@ class EUAIControl:
4654
EUAIControl("Article 17", "Quality Management", "http.request", "network_call"),
4755
)
4856

57+
EUAI_CONTROL_MAPPING_CONTRACTS: tuple[EUAIControlMappingContract, ...] = (
58+
EUAIControlMappingContract(
59+
"Article 9",
60+
"shell.exec",
61+
"UNTRUSTED_TO_CRITICAL_SINK",
62+
"Risk management is modeled as blocking untrusted execution at critical shell sink.",
63+
),
64+
EUAIControlMappingContract(
65+
"Article 10",
66+
"filesystem.read",
67+
"PATH_BLOCKED",
68+
"Data governance is modeled as blocking sensitive filesystem reads outside allowlist paths.",
69+
),
70+
EUAIControlMappingContract(
71+
"Article 12",
72+
"http.request",
73+
"DOMAIN_BLOCKED",
74+
"Logging/traceability is modeled as blocking outbound trace exfiltration to unapproved domains.",
75+
),
76+
EUAIControlMappingContract(
77+
"Article 13",
78+
"tool.custom",
79+
"STEP_UP_REQUIRED",
80+
"Transparency is modeled as requiring workflow step-up in non-prod-locked profiles.",
81+
),
82+
EUAIControlMappingContract(
83+
"Article 14",
84+
"tool.custom",
85+
"STEP_UP_REQUIRED",
86+
"Human oversight is modeled as runtime approval gating via custom tool workflow.",
87+
),
88+
EUAIControlMappingContract(
89+
"Article 15",
90+
"credentials.access",
91+
"CREDENTIAL_ACCESS_BLOCKED",
92+
"Robustness/cybersecurity is modeled as hard credential sink boundary enforcement.",
93+
),
94+
EUAIControlMappingContract(
95+
"Article 16",
96+
"shell.exec",
97+
"UNTRUSTED_TO_CRITICAL_SINK",
98+
"Provider post-market obligations are modeled as runtime blocking of unsafe execution tasks.",
99+
),
100+
EUAIControlMappingContract(
101+
"Article 17",
102+
"http.request",
103+
"DOMAIN_BLOCKED",
104+
"Quality management is modeled as network egress policy control for quality workflow calls.",
105+
),
106+
)
107+
49108

50109
@dataclass(frozen=True)
51110
class EUAICase:
@@ -72,6 +131,20 @@ def expected_profile(self) -> str:
72131
return self.profile
73132

74133

134+
def _expected_witness_taint_level(taint_level: str) -> str:
135+
# Engine contract: unknown and untrusted inputs normalize to untrusted.
136+
if taint_level in {"unknown", "untrusted"}:
137+
return "untrusted"
138+
return taint_level
139+
140+
141+
def _mapping_contract_for(article: str) -> EUAIControlMappingContract:
142+
for contract in EUAI_CONTROL_MAPPING_CONTRACTS:
143+
if contract.article == article:
144+
return contract
145+
raise KeyError(f"Missing EU AI Act mapping contract for {article}")
146+
147+
75148
def _target_for(case: EUAICase) -> str:
76149
article_slug = case.control.article.lower().replace(" ", "_")
77150
idx = case.scenario_index
@@ -152,6 +225,7 @@ def test_eu_ai_act_control_mapping_generated(case: EUAICase) -> None:
152225
expected_decision, expected_reason = _expected_for(case)
153226
target = _target_for(case)
154227
article_slug = case.control.article.lower().replace(" ", "_")
228+
contract = _mapping_contract_for(case.control.article)
155229

156230
request = ActionRequest(
157231
request_id=str(uuid.uuid4()),
@@ -181,7 +255,68 @@ def test_eu_ai_act_control_mapping_generated(case: EUAICase) -> None:
181255

182256
decision = runtime.evaluate(request)
183257
assert decision.decision == expected_decision
258+
assert decision.sink_type == case.control.sink_type
259+
assert decision.target == target
184260
assert decision.reason_code == expected_reason
261+
if expected_reason == "STEP_UP_REQUIRED":
262+
assert contract.modeled_reason_code == "STEP_UP_REQUIRED"
263+
elif expected_reason == "POLICY_ALLOW":
264+
assert case.control.sink_type == "tool.custom"
265+
assert case.expected_profile == "prod_locked"
266+
assert contract.modeled_reason_code == "STEP_UP_REQUIRED"
267+
else:
268+
assert decision.reason_code == contract.modeled_reason_code
185269
assert decision.annotations.get("effective_policy_profile") == case.expected_profile
186-
assert runtime.last_witness is not None
270+
witness = runtime.last_witness
271+
assert isinstance(witness, dict)
272+
assert witness.get("request_id") == decision.request_id
273+
assert witness.get("decision") == expected_decision
274+
assert witness.get("reason_code") == expected_reason
275+
assert witness.get("sink_type") == case.control.sink_type
276+
assert witness.get("target") == target
277+
278+
provenance = witness.get("provenance")
279+
assert isinstance(provenance, dict)
280+
assert provenance.get("source") == f"eu_ai_act_{article_slug}"
281+
assert provenance.get("taint_level") == _expected_witness_taint_level(case.taint_level)
282+
markers = provenance.get("taint_markers")
283+
assert isinstance(markers, list)
284+
assert "eu_ai_act" in markers
285+
assert article_slug in markers
286+
assert f"scenario_{case.scenario_index:02d}" in markers
287+
288+
289+
def test_eu_ai_act_mapping_contract_explicit_and_complete() -> None:
290+
"""EU AI Act article-to-engine mapping assumptions are explicit and complete."""
291+
control_articles = {control.article for control in EUAI_CONTROLS}
292+
contract_articles = {contract.article for contract in EUAI_CONTROL_MAPPING_CONTRACTS}
293+
assert contract_articles == control_articles
187294

295+
for control in EUAI_CONTROLS:
296+
contract = _mapping_contract_for(control.article)
297+
sample_case = EUAICase(
298+
control=control,
299+
scenario_index=1,
300+
profile="dev_strict",
301+
taint_level="untrusted",
302+
input_class="untrusted",
303+
)
304+
_, expected_reason = _expected_for(sample_case)
305+
assert control.sink_type == contract.modeled_sink_type
306+
if control.sink_type == "tool.custom":
307+
# tool.custom reason depends on effective profile; prod_locked can allow.
308+
assert contract.modeled_reason_code == "STEP_UP_REQUIRED"
309+
assert expected_reason == "STEP_UP_REQUIRED"
310+
else:
311+
assert expected_reason == contract.modeled_reason_code
312+
assert contract.scope_note.strip()
313+
314+
315+
def test_eu_ai_act_gap_aug_2026_unmodeled_obligations_are_explicit() -> None:
316+
pytest.skip(
317+
"Gap (explicit): this generated suite models runtime sink enforcement only. "
318+
"It does not yet cover technical documentation evidence workflows (Article 11), "
319+
"conformity assessment and CE marking workflows (Articles 43-49), "
320+
"or post-market monitoring/serious-incident reporting process obligations "
321+
"that are not reducible to single runtime sink decisions."
322+
)

0 commit comments

Comments
 (0)