Files
breakpilot-compliance/backend-compliance/tests/test_regulatory_map.py
T
Benjamin Admin 9312ad18ef feat(regulatory-map): customer-readable read-model over the scope (step 4)
The Map Renderer explains the engine's state, it does not extend it. Pure
composition of resolve_product_scope (scope verdict) + derive_obligations
(registry-linked obligations + overlaps) into one RegulatoryMap.

- product_summary, trigger_facts, applicable/uncertain/excluded regulations,
  unsupported_domains, overlaps (shared_obligations), shared_evidence, and a
  customer-readable executive_summary.
- No own legal decisions: applicable/uncertain mirror the scope verdict exactly.
- Obligations shown ONLY when registry-linkable (registry_anchor) — MaschinenVO/
  EMV obligations are proposed, so they render empty + a note, never as linked.
  Overlaps/shared_evidence likewise filtered to registry-linked members.
- Uncertain regulations link to the navigator question that would resolve them
  (RED -> has_radio_module, DataAct -> generates_usage_data).
- Environmental appears only as unsupported_domain; executive_summary has NO
  percentage (counts + "no further regulations identified" instead).
- POST /reasoning/regulatory-map (thin handler). Response types are presentation-
  level, not meta-model classes (freeze v1.0 untouched).
- 9 tests; 56 green (existing reasoning MVP stays green), mypy clean, LOC ok.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 10:36:06 +02:00

160 lines
6.0 KiB
Python

"""Tests for the Regulatory Map renderer (step 4).
Acceptance: the renderer makes no own legal decisions (it composes the scope +
registry-linked obligations); CRA/MaschVO/EMV are separate; RED/DataAct/NIS2 are
uncertain; environmental is unsupported (not applicable); obligations appear only
when registry-linkable; the executive summary has no percentage.
"""
from __future__ import annotations
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from compliance.product_scope import resolve_product_scope
from compliance.profile.canonical import (
CanonicalLifecyclePhase,
CanonicalProductRegulatoryProfile,
CanonicalProductType,
EconomicOperatorRole,
EnvironmentalImpact,
)
from compliance.regulatory_map import render_regulatory_map
_PROPOSED_IDS = {
"machine_risk_assessment", "machine_safety_control_systems", "machine_protection_against_corruption",
"machine_instructions_for_use", "machine_ce_conformity", "data_act_data_access_by_design",
"data_act_user_data_access", "cra_secure_by_design", "cra_risk_assessment",
"cra_technical_documentation", "cra_ce_conformity_assessment", "cra_instructions_for_use",
}
def ready_profile(**ov) -> CanonicalProductRegulatoryProfile:
base = dict(
name="Industriespülmaschine",
product_type=CanonicalProductType.MACHINERY,
markets=["EU", "DE"],
economic_operator_role=EconomicOperatorRole.MANUFACTURER,
lifecycle_phase=CanonicalLifecyclePhase.PLACING_ON_MARKET,
is_machine=True,
is_component=False,
has_software_updates=True,
has_embedded_software=True,
has_remote_access=True,
technologies=["cloud", "ota_updates"],
)
base.update(ov)
return CanonicalProductRegulatoryProfile(**base)
# 1. renderer makes no own decisions — it mirrors the scope verdict exactly.
def test_no_own_legal_decisions():
p = ready_profile()
m = render_regulatory_map(p)
scope = resolve_product_scope(p).regulatory_scope
assert {v.regulation_id for v in m.applicable_regulations} == {
r.regulation_id for r in scope.applicable_regulations
}
assert {v.regulation_id for v in m.uncertain_regulations} == {
r.regulation_id for r in scope.uncertain_regulations
}
# 2/3/5. CRA/MaschVO/EMV separate applicable; RED/DataAct/NIS2 uncertain.
def test_regulation_separation():
m = render_regulatory_map(ready_profile())
applicable = {v.regulation_id for v in m.applicable_regulations}
uncertain = {v.regulation_id for v in m.uncertain_regulations}
assert {"CRA", "MaschinenVO", "EMV"} <= applicable
assert {"RED", "DataAct", "NIS2"} <= uncertain
# 4. environmental triggers surface as unsupported_domain, never applicable.
def test_environmental_unsupported_not_applicable():
p = ready_profile(environmental=EnvironmentalImpact(discharges_to_wastewater=True, uses_cleaning_chemicals=True))
m = render_regulatory_map(p)
domains = {d.domain for d in m.unsupported_domains}
assert "environment_water" in domains and "chemicals" in domains
assert all(v.regulation_id in {"CRA", "MaschinenVO", "RED", "DataAct", "EMV", "NIS2"} for v in m.applicable_regulations)
# 6. obligations are shown only when a registry id is linkable.
def test_obligations_only_registry_linkable():
m = render_regulatory_map(ready_profile())
shown = {o.obligation_id for v in m.applicable_regulations for o in v.obligations}
assert shown # CRA registry obligations are shown
assert "sbom_creation" in shown
assert not (shown & _PROPOSED_IDS) # no proposed (non-registry) obligation leaks in
# MaschinenVO is applicable but its obligations are proposed -> empty + note
machvo = next(v for v in m.applicable_regulations if v.regulation_id == "MaschinenVO")
assert machvo.obligations == []
assert machvo.obligations_note
# 7. executive summary contains no percentage.
def test_executive_summary_no_percent():
m = render_regulatory_map(ready_profile())
assert "%" not in m.executive_summary
assert "prozent" not in m.executive_summary.lower()
# 8. output is customer-readable and structured.
def test_customer_readable():
m = render_regulatory_map(ready_profile())
assert m.product_summary
assert "wahrscheinlich" in m.executive_summary
assert "Unsicher" in m.executive_summary
assert m.trigger_facts
# needs-facts profile -> map says scope not yet resolved.
def test_needs_facts_map():
m = render_regulatory_map(CanonicalProductRegulatoryProfile(name="X"))
assert m.scope_resolved is False
assert "Mindestfakten" in m.executive_summary
assert m.applicable_regulations == []
# uncertain RED links to the radio navigator question.
def test_uncertain_links_to_navigator_question():
m = render_regulatory_map(ready_profile())
red = next(v for v in m.uncertain_regulations if v.regulation_id == "RED")
assert "has_radio_module" in red.question_refs
# endpoint smoke.
@pytest.fixture(scope="module")
def client():
from compliance.api.reasoning_routes import router
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_endpoint_regulatory_map(client):
r = client.post(
"/reasoning/regulatory-map",
json={
"product_profile": {
"name": "M",
"product_type": "machinery",
"markets": ["EU"],
"economic_operator_role": "manufacturer",
"lifecycle_phase": "placing_on_market",
"is_machine": True,
"is_component": False,
"has_software_updates": True,
"has_embedded_software": True,
"has_remote_access": True,
"technologies": ["cloud"],
}
},
)
assert r.status_code == 200
body = r.json()
assert body["scope_resolved"] is True
assert {v["regulation_id"] for v in body["applicable_regulations"]} >= {"CRA", "MaschinenVO"}
assert "%" not in body["executive_summary"]