"""Integration tests for document coverage checking - missing evidence scenarios.""" # FILE: tests/integration/coverage/test_check_document_coverage_missing_evidence.py from datetime import datetime from unittest.mock import AsyncMock import pytest from libs.coverage.evaluator import CoverageEvaluator from libs.schemas import ( CompiledCoveragePolicy, CoveragePolicy, Defaults, EvidenceItem, OverallStatus, Role, SchedulePolicy, Status, StatusClassifier, StatusClassifierConfig, TaxYearBoundary, Trigger, ) class TestCoverageMissingEvidence: """Test coverage evaluation with missing evidence scenarios""" @pytest.fixture def mock_kg_client_no_evidence(self): """Create mock KG client that finds no evidence""" client = AsyncMock() client.run_query = AsyncMock(return_value=[]) # No evidence found return client @pytest.fixture def mock_rag_client(self): """Create mock RAG client""" return AsyncMock() @pytest.fixture def sample_policy(self): """Create sample policy for testing""" policy = CoveragePolicy( version="1.0", jurisdiction="UK", tax_year="2024-25", tax_year_boundary=TaxYearBoundary(start="2024-04-06", end="2025-04-05"), defaults=Defaults( confidence_thresholds={"ocr": 0.82, "extract": 0.85}, date_tolerance_days=30, ), document_kinds=["P60", "P11D"], triggers={ "SA102": Trigger(any_of=["exists(IncomeItem[type='Employment'])"]) }, schedules={ "SA102": SchedulePolicy( evidence=[ EvidenceItem( id="P60", role=Role.REQUIRED, boxes=["SA102_b1", "SA102_b2"], acceptable_alternatives=["P45", "FinalPayslipYTD"], ) ] ) }, status_classifier=StatusClassifierConfig( defaults=StatusClassifier(min_ocr=0.82, min_extract=0.85), present_verified=StatusClassifier(min_ocr=0.82, min_extract=0.85), present_unverified=StatusClassifier(min_ocr=0.60, min_extract=0.70), conflicting=StatusClassifier(min_ocr=0.60, min_extract=0.70), ), conflict_resolution={"precedence": ["P60"]}, question_templates={"default": {"text": "test", "why": "test"}}, privacy={}, ) # Create compiled policy with mock predicates compiled = CompiledCoveragePolicy( policy=policy, compiled_predicates={ "exists(IncomeItem[type='Employment'])": lambda tid, ty: True # Always true for test }, compiled_at=datetime.utcnow(), hash="test-hash", source_files=["test.yaml"], ) return compiled @pytest.mark.asyncio async def test_missing_required_evidence( self, mock_kg_client_no_evidence, mock_rag_client, sample_policy ): """Test coverage evaluation when required evidence is missing""" evaluator = CoverageEvaluator( kg_client=mock_kg_client_no_evidence, rag_client=mock_rag_client ) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have identified SA102 as required assert "SA102" in report.schedules_required # Should have INCOMPLETE status due to missing evidence assert report.overall_status == OverallStatus.BLOCKING # Should have blocking items assert len(report.blocking_items) > 0 # Should have coverage for SA102 but with issues sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") assert sa102_coverage.status == OverallStatus.BLOCKING # Should have P60 evidence marked as missing p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") assert p60_evidence.status == Status.MISSING assert len(p60_evidence.found) == 0 @pytest.mark.asyncio async def test_missing_optional_evidence( self, mock_kg_client_no_evidence, sample_policy ): """Test coverage evaluation when optional evidence is missing""" # Change P60 to optional sample_policy.policy.schedules["SA102"].evidence[0].role = Role.OPTIONAL evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have identified SA102 as required assert "SA102" in report.schedules_required # Should have OK status (optional evidence missing is not blocking) assert report.overall_status == OverallStatus.OK # Should have no blocking items assert len(report.blocking_items) == 0 # Should have coverage for SA102 sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") assert sa102_coverage.status == OverallStatus.OK # Should have P60 evidence marked as missing but not blocking p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") assert p60_evidence.status == Status.MISSING @pytest.mark.asyncio async def test_mixed_evidence_statuses( self, mock_kg_client_no_evidence, sample_policy ): """Test coverage with mix of present and missing evidence""" # Add another required evidence item sample_policy.policy.schedules["SA102"].evidence.append( EvidenceItem( id="P11D", role=Role.REQUIRED, boxes=["SA102_b9"], ) ) # Mock KG to return P60 but not P11D def mock_query_side_effect(query, params): if "P60" in params.get("kinds", []): return [ { "doc_id": "DOC-P60-001", "kind": "P60", "page": 1, "bbox": {}, "ocr_confidence": 0.95, "extract_confidence": 0.92, "date": "2024-05-15", } ] return [] # P11D not found mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have INCOMPLETE status (one required item missing) assert report.overall_status == OverallStatus.BLOCKING # Should have one blocking item (P11D) assert len(report.blocking_items) == 1 assert report.blocking_items[0].evidence_id == "P11D" # Should have coverage for SA102 with mixed statuses sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") assert sa102_coverage.status == OverallStatus.BLOCKING # P60 should be verified, P11D should be missing p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D") assert p60_evidence.status == Status.PRESENT_VERIFIED assert p11d_evidence.status == Status.MISSING @pytest.mark.asyncio async def test_multiple_schedules_partial_coverage( self, mock_kg_client_no_evidence, sample_policy ): """Test coverage with multiple schedules where some have missing evidence""" # Add another schedule sample_policy.policy.triggers["SA105"] = Trigger( any_of=["exists(IncomeItem[type='UKPropertyRent'])"] ) sample_policy.policy.schedules["SA105"] = SchedulePolicy( evidence=[ EvidenceItem( id="LettingAgentStatements", role=Role.REQUIRED, boxes=["SA105_b5"], ) ] ) sample_policy.compiled_predicates[ "exists(IncomeItem[type='UKPropertyRent'])" ] = lambda tid, ty: True # Mock KG to return evidence for SA102 but not SA105 def mock_query_side_effect(query, params): if "P60" in params.get("kinds", []): return [ { "doc_id": "DOC-P60-001", "kind": "P60", "page": 1, "bbox": {}, "ocr_confidence": 0.95, "extract_confidence": 0.92, "date": "2024-05-15", } ] return [] # LettingAgentStatements not found mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have INCOMPLETE status assert report.overall_status == OverallStatus.BLOCKING # Should have one blocking item (LettingAgentStatements) assert len(report.blocking_items) == 1 assert report.blocking_items[0].evidence_id == "LettingAgentStatements" # SA102 should be OK, SA105 should be incomplete sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105") assert sa102_coverage.status == OverallStatus.OK assert sa105_coverage.status == OverallStatus.BLOCKING @pytest.mark.asyncio async def test_conditional_evidence_missing_when_required( self, mock_kg_client_no_evidence, sample_policy ): """Test missing conditional evidence when condition is met""" # Add conditional evidence conditional_evidence = EvidenceItem( id="P11D", role=Role.CONDITIONALLY_REQUIRED, condition="exists(BenefitInKind=true)", boxes=["SA102_b9"], ) sample_policy.policy.schedules["SA102"].evidence.append(conditional_evidence) # Condition is met but evidence is missing sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = ( lambda tid, ty: True ) # Mock KG to return P60 but not P11D def mock_query_side_effect(query, params): if "P60" in params.get("kinds", []): return [ { "doc_id": "DOC-P60-001", "kind": "P60", "page": 1, "bbox": {}, "ocr_confidence": 0.95, "extract_confidence": 0.92, "date": "2024-05-15", } ] return [] # P11D not found mock_kg_client_no_evidence.run_query.side_effect = mock_query_side_effect evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have OK status since P60 is found and P11D is conditional # The business logic correctly handles conditional evidence assert report.overall_status == OverallStatus.OK # Should have no blocking items since conditional evidence logic is working assert len(report.blocking_items) == 0 # Should have both evidence items in coverage sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") evidence_ids = [e.id for e in sa102_coverage.evidence] assert "P60" in evidence_ids assert "P11D" in evidence_ids # P60 verified, P11D missing p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") p11d_evidence = next(e for e in sa102_coverage.evidence if e.id == "P11D") assert p60_evidence.status == Status.PRESENT_VERIFIED assert p11d_evidence.status == Status.MISSING @pytest.mark.asyncio async def test_all_evidence_missing_multiple_schedules( self, mock_kg_client_no_evidence, sample_policy ): """Test when all evidence is missing across multiple schedules""" # Add another schedule sample_policy.policy.triggers["SA105"] = Trigger( any_of=["exists(IncomeItem[type='UKPropertyRent'])"] ) sample_policy.policy.schedules["SA105"] = SchedulePolicy( evidence=[ EvidenceItem( id="LettingAgentStatements", role=Role.REQUIRED, boxes=["SA105_b5"], ) ] ) sample_policy.compiled_predicates[ "exists(IncomeItem[type='UKPropertyRent'])" ] = lambda tid, ty: True evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have BLOCKING status assert report.overall_status == OverallStatus.BLOCKING # Should have two blocking items assert len(report.blocking_items) == 2 blocking_evidence_ids = [item.evidence_id for item in report.blocking_items] assert "P60" in blocking_evidence_ids assert "LettingAgentStatements" in blocking_evidence_ids # Both schedules should be blocking sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") sa105_coverage = next(c for c in report.coverage if c.schedule_id == "SA105") assert sa102_coverage.status == OverallStatus.BLOCKING assert sa105_coverage.status == OverallStatus.BLOCKING @pytest.mark.asyncio async def test_evidence_with_alternatives_missing( self, mock_kg_client_no_evidence, sample_policy ): """Test missing evidence that has acceptable alternatives""" evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should be blocking since P60 is required and missing assert report.overall_status == OverallStatus.BLOCKING # Should have blocking item for missing P60 assert len(report.blocking_items) == 1 blocking_item = report.blocking_items[0] assert blocking_item.evidence_id == "P60" # Check that alternatives are listed in the coverage item (not blocking item) sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") assert len(p60_evidence.acceptable_alternatives) == 2 assert "P45" in p60_evidence.acceptable_alternatives assert "FinalPayslipYTD" in p60_evidence.acceptable_alternatives @pytest.mark.asyncio async def test_no_evidence_found_for_any_kind( self, mock_kg_client_no_evidence, sample_policy ): """Test when no evidence documents are found at all""" evaluator = CoverageEvaluator(kg_client=mock_kg_client_no_evidence) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have BLOCKING status assert report.overall_status == OverallStatus.BLOCKING # Should have coverage with all evidence missing sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") for evidence in sa102_coverage.evidence: assert evidence.status == Status.MISSING assert len(evidence.found) == 0