"""Integration tests for document coverage checking - happy path scenarios.""" # FILE: tests/integration/coverage/test_check_document_coverage_happy_path.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 TestCoverageHappyPath: """Test coverage evaluation happy path scenarios""" @pytest.fixture def mock_kg_client(self): """Create mock KG client""" client = AsyncMock() # Mock successful evidence finding client.run_query = AsyncMock( return_value=[ { "doc_id": "DOC-P60-001", "kind": "P60", "page": 1, "bbox": {"x": 100, "y": 200, "width": 300, "height": 50}, "ocr_confidence": 0.95, "extract_confidence": 0.92, "date": "2024-05-15", } ] ) 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_complete_coverage_happy_path( self, mock_kg_client, mock_rag_client, sample_policy ): """Test complete coverage evaluation with all evidence present""" evaluator = CoverageEvaluator( kg_client=mock_kg_client, 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 overall OK status 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 verified p60_evidence = next(e for e in sa102_coverage.evidence if e.id == "P60") assert p60_evidence.status == Status.PRESENT_VERIFIED assert len(p60_evidence.found) == 1 assert p60_evidence.found[0].doc_id == "DOC-P60-001" @pytest.mark.asyncio async def test_infer_required_schedules(self, mock_kg_client, sample_policy): """Test schedule inference based on triggers""" evaluator = CoverageEvaluator(kg_client=mock_kg_client) required = await evaluator.infer_required_schedules( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should identify SA102 as required (predicate returns True) assert "SA102" in required @pytest.mark.asyncio async def test_find_evidence_docs(self, mock_kg_client, sample_policy): """Test evidence document finding""" evaluator = CoverageEvaluator(kg_client=mock_kg_client) evidence_map = await evaluator.find_evidence_docs( taxpayer_id="T-001", tax_year="2024-25", evidence_ids=["P60"], policy=sample_policy, ) # Should find P60 evidence assert "P60" in evidence_map assert len(evidence_map["P60"]) == 1 found_evidence = evidence_map["P60"][0] assert found_evidence.doc_id == "DOC-P60-001" assert found_evidence.kind == "P60" assert found_evidence.ocr_confidence == 0.95 @pytest.mark.asyncio async def test_build_reason_and_citations( self, mock_kg_client, mock_rag_client, sample_policy ): """Test reason and citation building""" evaluator = CoverageEvaluator( kg_client=mock_kg_client, rag_client=mock_rag_client ) # Mock KG citations mock_kg_client.run_query.return_value = [ { "rule_id": "UK.SA102.P60.Required", "doc_id": "SA102-Notes-2025", "locator": "p.3 ยง1.1", } ] evidence_item = sample_policy.policy.schedules["SA102"].evidence[0] reason, citations = await evaluator.build_reason_and_citations( schedule_id="SA102", evidence_item=evidence_item, status=Status.PRESENT_VERIFIED, taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should build appropriate reason assert "P60" in reason assert "verified" in reason.lower() # Should have citations assert len(citations) > 0 @pytest.mark.asyncio async def test_multiple_schedules_coverage(self, mock_kg_client, sample_policy): """Test coverage evaluation with multiple schedules""" # Add another schedule to policy 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"], ) ] ) # Add predicate for SA105 sample_policy.compiled_predicates[ "exists(IncomeItem[type='UKPropertyRent'])" ] = lambda tid, ty: True # Mock evidence for both schedules 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", } ] elif "LettingAgentStatements" in params.get("kinds", []): return [ { "doc_id": "DOC-AGENT-001", "kind": "LettingAgentStatements", "page": 1, "bbox": {}, "ocr_confidence": 0.88, "extract_confidence": 0.90, "date": "2024-06-01", } ] return [] mock_kg_client.run_query.side_effect = mock_query_side_effect evaluator = CoverageEvaluator(kg_client=mock_kg_client) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should identify both schedules as required assert "SA102" in report.schedules_required assert "SA105" in report.schedules_required # Should have coverage for both schedules assert len(report.coverage) == 2 # Both should be OK assert report.overall_status == OverallStatus.OK @pytest.mark.asyncio async def test_conditional_evidence_not_required( self, mock_kg_client, sample_policy ): """Test that conditional evidence is skipped when condition not met""" # Add conditional evidence to SA102 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) # Add predicate that returns False (condition not met) sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = ( lambda tid, ty: False ) evaluator = CoverageEvaluator(kg_client=mock_kg_client) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have SA102 coverage sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") # Should only have P60 evidence (P11D should be skipped) evidence_ids = [e.id for e in sa102_coverage.evidence] assert "P60" in evidence_ids assert "P11D" not in evidence_ids @pytest.mark.asyncio async def test_conditional_evidence_required(self, mock_kg_client, sample_policy): """Test that conditional evidence is included when condition is met""" # Add conditional evidence to SA102 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) # Add predicate that returns True (condition met) sample_policy.compiled_predicates["exists(BenefitInKind=true)"] = ( lambda tid, ty: True ) # Mock evidence finding for 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", } ] elif "P11D" in params.get("kinds", []): return [ { "doc_id": "DOC-P11D-001", "kind": "P11D", "page": 1, "bbox": {}, "ocr_confidence": 0.90, "extract_confidence": 0.88, "date": "2024-07-06", } ] return [] mock_kg_client.run_query.side_effect = mock_query_side_effect evaluator = CoverageEvaluator(kg_client=mock_kg_client) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have SA102 coverage sa102_coverage = next(c for c in report.coverage if c.schedule_id == "SA102") # Should have both P60 and P11D evidence evidence_ids = [e.id for e in sa102_coverage.evidence] assert "P60" in evidence_ids assert "P11D" in evidence_ids # Both should be verified 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.PRESENT_VERIFIED @pytest.mark.asyncio async def test_no_schedules_required(self, mock_kg_client, sample_policy): """Test coverage when no schedules are required""" # Make predicate return False (no employment income) sample_policy.compiled_predicates["exists(IncomeItem[type='Employment'])"] = ( lambda tid, ty: False ) evaluator = CoverageEvaluator(kg_client=mock_kg_client) report = await evaluator.check_document_coverage( taxpayer_id="T-001", tax_year="2024-25", policy=sample_policy, ) # Should have no required schedules assert len(report.schedules_required) == 0 # Should have OK status (nothing required, nothing missing) assert report.overall_status == OverallStatus.OK # Should have no coverage items assert len(report.coverage) == 0 # Should have no blocking items assert len(report.blocking_items) == 0