"""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"]