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,414 @@
|
||||
"""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
|
||||
@@ -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