"""Tests for the Product Regulatory Navigator (missing-facts layer). Acceptance: a well-filled company-profile yields <= 10 questions; known facts are not re-asked; environmental questions are trigger-only (no law evaluation); the Navigator decides which facts are missing, NOT what applies. """ from __future__ import annotations from compliance.navigator import NavigatorResult, apply_answers, navigate from compliance.navigator.questions import QUESTION_CATALOG, QuestionPriority from compliance.profile import from_company_profile from compliance.profile.canonical import CanonicalProductRegulatoryProfile, EconomicOperatorRole COMPANY = { "industry": "Maschinenbau", "business_model": "B2B", "company_size": "medium", "target_markets": ["DE", "EU"], "primary_jurisdiction": "DE", "machine_builder": { "productTypes": ["special_machine"], "containsFirmware": True, "hasSafetyFunction": True, "isNetworked": True, "hasRemoteAccess": True, "hasOTAUpdates": True, "hasRiskAssessment": True, }, } def _empty() -> CanonicalProductRegulatoryProfile: return CanonicalProductRegulatoryProfile(name="X") # 1. well-filled company-profile -> at most 10 questions. def test_filled_company_profile_at_most_10_questions(): result = navigate(from_company_profile(COMPANY)) assert len(result.suggested_questions) <= 10 # 2. known facts (markets, is_machine) are not re-asked; true gaps still are. def test_known_facts_not_reasked(): result = navigate(from_company_profile(COMPANY)) assert "markets" not in result.missing_facts assert "is_machine" not in result.missing_facts # genuine gaps the company-profile cannot provide are still surfaced assert "economic_operator_role" in result.missing_facts assert "has_radio_module" in result.missing_facts # 3. environmental questions are trigger-only — no environmental-law evaluation. def test_environmental_questions_are_triggers_only(): result = navigate(_empty()) env = [q for q in result.suggested_questions if q.target_field.startswith("environmental.")] assert len(env) >= 3 assert all(q.answer_type.value == "bool" for q in env) # 4. the Navigator decides only missing facts, never what applies. def test_navigator_decides_only_missing_facts(): assert set(NavigatorResult.model_fields.keys()) == { "missing_facts", "suggested_questions", "completeness_summary", } # no question carries a verdict — only metadata about what it would unblock for q in QUESTION_CATALOG: assert q.regulatory_domains_unblocked # metadata, not a decision assert hasattr(q, "answer_type") # 5. apply_answers updates the profile; answered facts drop out of missing. def test_apply_answers_updates_profile(): profile = from_company_profile(COMPANY) updated = apply_answers( profile, { "economic_operator_role": "manufacturer", "markets": ["DE", "US"], "has_radio_module": True, "env_wastewater": True, }, ) assert updated.economic_operator_role == EconomicOperatorRole.MANUFACTURER assert updated.markets == ["DE", "US"] assert updated.has_radio_module is True assert updated.environmental.discharges_to_wastewater is True after = navigate(updated) assert "economic_operator_role" not in after.missing_facts assert "has_radio_module" not in after.missing_facts assert "environmental.discharges_to_wastewater" not in after.missing_facts # 6. questions are ordered P0 -> P1 -> P2. def test_priority_ordering(): questions = navigate(_empty()).suggested_questions orders = [q.order() for q in questions] assert orders == sorted(orders) assert questions[0].priority == QuestionPriority.P0 # 7. ready_for_scope flips once all P0 facts are answered. def test_ready_for_scope_after_p0(): profile = _empty() assert navigate(profile).completeness_summary.ready_for_scope is False answered = apply_answers( profile, { "markets": ["DE"], "economic_operator_role": "manufacturer", "lifecycle_phase": "placing_on_market", "is_machine": True, "is_component": False, }, ) summary = navigate(answered).completeness_summary assert summary.ready_for_scope is True # 8. empty profile asks the full (bounded) catalog. def test_empty_profile_bounded_catalog(): result = navigate(_empty()) assert len(result.suggested_questions) == len(QUESTION_CATALOG) assert result.completeness_summary.total_relevant == len(QUESTION_CATALOG)