"""Tests for the Product Profile convergence layer. Covers the 10 acceptance criteria of the CanonicalProductRegulatoryProfile spec: lossless ProductWizard mapping, company-profile prefill, AI stays delegated, markets no longer hardcoded, and the new Navigator fields (role/radio/usage-data/ lifecycle/BOM) plus one-semantic-profile across reasoning + gap. """ from __future__ import annotations from compliance.profile import ( CanonicalLifecyclePhase, CanonicalProductRegulatoryProfile, CanonicalProductType, ComponentKind, EconomicOperatorRole, ProductComponent, from_company_profile, from_product_wizard, to_gap_profile, to_reasoning_profile, ) from compliance.reasoning import discover_scope from compliance.reasoning.enums import ManufacturerRole, ProductLifecyclePhase # A realistic ProductWizard payload — exactly the gap.ProductProfile JSON shape. WIZARD = { "name": "Industriespülmaschine", "description": "vernetzte Spülmaschine", "product_type": "machinery", "technologies": ["cloud", "ota_updates", "sensor", "actuator"], "data_processing": ["telemetry"], "markets": ["EU"], "connected_to_internet": True, "has_software_updates": True, "uses_ai": False, "processes_personal_data": False, "is_critical_infra_supplier": False, "existing_certifications": ["CE"], "applied_norms": ["ISO12100"], "has_risk_assessment": True, "has_technical_file": True, "has_operating_manual": True, "has_sbom": False, "has_vuln_management": False, "has_update_mechanism": True, "has_incident_response": False, "has_supply_chain_mgmt": False, "ce_marking_since": "", "product_age": "5", } COMPANY = { "company_name": "ACME Maschinen GmbH", "industry": "Maschinenbau", "business_model": "B2B", "company_size": "medium", "target_markets": ["DE", "EU"], "primary_jurisdiction": "DE", "headquarters_country": "DE", "uses_ai": False, "is_data_controller": True, "machine_builder": { "productDescription": "Industriespülmaschine", "productTypes": ["special_machine"], "containsSoftware": True, "containsFirmware": True, "containsAI": False, "hasSafetyFunction": True, "safetyFunctionDescription": "Türverriegelung", "isNetworked": True, "hasRemoteAccess": True, "hasOTAUpdates": True, "hasRiskAssessment": True, "criticalSectorClients": False, }, } # 1. ProductWizard data maps losslessly into the canonical and back to gap shape. def test_product_wizard_lossless_roundtrip(): canonical = from_product_wizard(WIZARD) assert to_gap_profile(canonical) == WIZARD # 2. company-profile can prefill the canonical profile. def test_company_profile_prefill(): c = from_company_profile(COMPANY) assert c.sector_industry == "Maschinenbau" assert c.b2b_or_b2c == "B2B" assert c.company_size == "medium" assert "DE" in c.markets and "EU" in c.markets assert c.has_safety_function is True assert c.has_remote_access is True assert c.has_embedded_software is True assert c.is_machine is True assert c.description == "Industriespülmaschine" # 3. AI-Act/ucca stays delegated — only uses_ai is forwarded, no risk classification. def test_ai_classification_stays_delegated(): c = CanonicalProductRegulatoryProfile(name="X", uses_ai=True) rp = to_reasoning_profile(c) assert rp.has_ai_functionality is True assert not hasattr(rp, "ai_risk_category") # no AI classification produced here # 4. markets are a real list, never hardcoded ['EU']. def test_markets_not_hardcoded_eu(): assert CanonicalProductRegulatoryProfile(name="X").markets == [] c = from_product_wizard({**WIZARD, "markets": ["US", "JP", "CA"]}) assert c.markets == ["US", "JP", "CA"] assert to_gap_profile(c)["markets"] == ["US", "JP", "CA"] assert to_reasoning_profile(c).eu_market is False # non-EU markets -> not EU # 5. economic-operator role exists and maps to the reasoning role. def test_economic_operator_role_exists(): c = CanonicalProductRegulatoryProfile(name="X", economic_operator_role=EconomicOperatorRole.IMPORTER) assert to_reasoning_profile(c).manufacturer_role == ManufacturerRole.IMPORTER # 6. radio_module exists (direct + inferred from a BOM component). def test_radio_module_exists(): assert to_reasoning_profile(CanonicalProductRegulatoryProfile(name="X", has_radio_module=True)).has_radio_module is True c = CanonicalProductRegulatoryProfile(name="X", components=[ProductComponent(name="WLAN", kind=ComponentKind.RADIO_MODULE)]) assert to_reasoning_profile(c).has_radio_module is True # 7. generates_usage_data exists (direct + inferred from telemetry). def test_generates_usage_data_exists(): c = CanonicalProductRegulatoryProfile(name="X", generates_usage_data=True) assert to_reasoning_profile(c).generates_usage_data is True inferred = from_product_wizard(WIZARD) # data_processing has telemetry assert to_reasoning_profile(inferred).generates_usage_data is True # 8. lifecycle_phase exists and maps. def test_lifecycle_phase_exists(): c = CanonicalProductRegulatoryProfile(name="X", lifecycle_phase=CanonicalLifecyclePhase.MAINTENANCE) assert to_reasoning_profile(c).lifecycle_phase == ProductLifecyclePhase.MAINTENANCE # 9. BOM components are structured. def test_bom_components_structured(): c = CanonicalProductRegulatoryProfile( name="Spülmaschine", components=[ ProductComponent(name="Umwälzpumpe", kind=ComponentKind.PUMP), ProductComponent(name="Heizung", kind=ComponentKind.HEATING), ProductComponent(name="SPS", kind=ComponentKind.PLC), ProductComponent(name="Abwasserablauf", kind=ComponentKind.WASTEWATER_OUTLET), ], ) kinds = {comp.kind for comp in c.components} assert ComponentKind.PLC in kinds and ComponentKind.WASTEWATER_OUTLET in kinds # 10. reasoning engine + gap engine run off ONE semantic profile. def test_one_semantic_profile_reasoning_and_gap(): canonical = CanonicalProductRegulatoryProfile( name="Industriespülmaschine", product_type=CanonicalProductType.MACHINERY, economic_operator_role=EconomicOperatorRole.MANUFACTURER, markets=["EU", "DE"], is_machine=True, has_safety_function=True, has_remote_access=True, has_software_updates=True, has_embedded_software=True, technologies=["cloud", "ota_updates"], ) gap = to_gap_profile(canonical) rp = to_reasoning_profile(canonical) # same facts, two projections assert gap["markets"] == ["EU", "DE"] assert rp.eu_market is True assert rp.has_remote_access is True assert rp.has_cloud_connection is True assert rp.is_machine is True assert rp.manufacturer_role == ManufacturerRole.MANUFACTURER # the projected reasoning profile actually drives the reasoning engine scope = discover_scope(rp) applicable = {r.regulation_id for r in scope.applicable_regulations} assert "CRA" in applicable assert "MaschinenVO" in applicable