Initial commit
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
Some checks failed
CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
CI/CD Pipeline / Policy Validation (push) Has been cancelled
CI/CD Pipeline / Test Suite (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-firm-connectors) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-forms) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-hmrc) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ingestion) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-normalize-map) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-ocr) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-indexer) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-reason) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (svc-rpa) (push) Has been cancelled
CI/CD Pipeline / Build Docker Images (ui-review) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-coverage) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-extract) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-kg) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (svc-rag-retriever) (push) Has been cancelled
CI/CD Pipeline / Security Scanning (ui-review) (push) Has been cancelled
CI/CD Pipeline / Generate SBOM (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / Notifications (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,435 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user