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
415 lines
14 KiB
Python
415 lines
14 KiB
Python
"""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
|