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
341 lines
10 KiB
Python
341 lines
10 KiB
Python
"""Pytest configuration and shared fixtures for coverage tests."""
|
|
|
|
# FILE: tests/conftest.py
|
|
|
|
import asyncio
|
|
import os
|
|
import tempfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from libs.schemas import (
|
|
CompiledCoveragePolicy,
|
|
ConflictRules,
|
|
CoveragePolicy,
|
|
Defaults,
|
|
EvidenceItem,
|
|
Privacy,
|
|
QuestionTemplates,
|
|
Role,
|
|
SchedulePolicy,
|
|
StatusClassifier,
|
|
StatusClassifierConfig,
|
|
TaxYearBoundary,
|
|
Trigger,
|
|
)
|
|
|
|
# pylint: disable=wrong-import-position,import-error,too-few-public-methods,global-statement
|
|
# pylint: disable=raise-missing-from,unused-argument,too-many-arguments,too-many-positional-arguments
|
|
# pylint: disable=too-many-locals,import-outside-toplevel
|
|
# mypy: disable-error-code=union-attr
|
|
# mypy: disable-error-code=no-untyped-def
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def event_loop():
|
|
"""Create an instance of the default event loop for the test session."""
|
|
loop = asyncio.get_event_loop_policy().new_event_loop()
|
|
yield loop
|
|
loop.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_config_dir():
|
|
"""Create temporary config directory with test policy files"""
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
config_dir = Path(temp_dir)
|
|
|
|
# Create baseline policy
|
|
baseline_policy = {
|
|
"version": "1.0",
|
|
"jurisdiction": "UK",
|
|
"tax_year": "2024-25",
|
|
"tax_year_boundary": {"start": "2024-04-06", "end": "2025-04-05"},
|
|
"defaults": {
|
|
"confidence_thresholds": {"ocr": 0.82, "extract": 0.85},
|
|
"date_tolerance_days": 30,
|
|
},
|
|
"document_kinds": ["P60", "P11D", "P45"],
|
|
"triggers": {
|
|
"SA102": {"any_of": ["exists(IncomeItem[type='Employment'])"]},
|
|
"SA105": {"any_of": ["exists(IncomeItem[type='UKPropertyRent'])"]},
|
|
},
|
|
"schedules": {
|
|
"SA102": {
|
|
"evidence": [
|
|
{
|
|
"id": "P60",
|
|
"role": "REQUIRED",
|
|
"boxes": ["SA102_b1", "SA102_b2"],
|
|
"acceptable_alternatives": ["P45", "FinalPayslipYTD"],
|
|
},
|
|
{
|
|
"id": "P11D",
|
|
"role": "CONDITIONALLY_REQUIRED",
|
|
"condition": "exists(BenefitInKind=true)",
|
|
"boxes": ["SA102_b9"],
|
|
},
|
|
]
|
|
},
|
|
"SA105": {
|
|
"evidence": [
|
|
{
|
|
"id": "LettingAgentStatements",
|
|
"role": "REQUIRED",
|
|
"boxes": ["SA105_b5"],
|
|
}
|
|
]
|
|
},
|
|
},
|
|
"status_classifier": {
|
|
"present_verified": {
|
|
"min_ocr": 0.82,
|
|
"min_extract": 0.85,
|
|
"date_in_year": True,
|
|
},
|
|
"present_unverified": {
|
|
"min_ocr": 0.60,
|
|
"min_extract": 0.70,
|
|
"date_in_year_or_tolerance": True,
|
|
},
|
|
"conflicting": {"conflict_rules": ["Same doc kind, different totals"]},
|
|
"missing": {"default": True},
|
|
},
|
|
"conflict_resolution": {"precedence": ["P60", "P11D"]},
|
|
"question_templates": {
|
|
"default": {
|
|
"text": "To complete the {schedule} for {tax_year}, we need {evidence}.",
|
|
"why": "{why}. See guidance: {guidance_doc}.",
|
|
}
|
|
},
|
|
"privacy": {"vector_pii_free": True, "redact_patterns": []},
|
|
}
|
|
|
|
with open(config_dir / "coverage.yaml", "w") as f:
|
|
yaml.dump(baseline_policy, f)
|
|
|
|
yield config_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_policy():
|
|
"""Create sample compiled 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(
|
|
present_verified=StatusClassifier(min_ocr=0.82, min_extract=0.85),
|
|
present_unverified=StatusClassifier(min_ocr=0.60, min_extract=0.70),
|
|
conflicting=StatusClassifier(conflict_rules=[]),
|
|
missing=StatusClassifier(conflict_rules=[]),
|
|
),
|
|
conflict_resolution=ConflictRules(precedence=["P60"]),
|
|
question_templates=QuestionTemplates(default={"text": "test", "why": "test"}),
|
|
privacy=Privacy(vector_pii_free=True, redact_patterns=[]),
|
|
)
|
|
|
|
# Create compiled policy with mock predicates
|
|
compiled = CompiledCoveragePolicy(
|
|
policy=policy,
|
|
compiled_predicates={
|
|
"exists(IncomeItem[type='Employment'])": lambda tid, ty: True
|
|
},
|
|
compiled_at=datetime.utcnow(),
|
|
hash="test-hash",
|
|
source_files=["test.yaml"],
|
|
)
|
|
|
|
return compiled
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_kg_client():
|
|
"""Create mock KG client for testing"""
|
|
client = AsyncMock()
|
|
|
|
# Default 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():
|
|
"""Create mock RAG client for testing"""
|
|
client = AsyncMock()
|
|
|
|
# Default citation search results
|
|
client.search = AsyncMock(
|
|
return_value=[
|
|
{
|
|
"doc_id": "SA102-Notes-2025",
|
|
"locator": "p.3 §1.1",
|
|
"url": "https://docs.local/SA102-Notes-2025#p3s1.1",
|
|
}
|
|
]
|
|
)
|
|
|
|
return client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_db_session():
|
|
"""Create mock database session for testing"""
|
|
session = AsyncMock()
|
|
|
|
# Mock database operations
|
|
session.add = MagicMock()
|
|
session.commit = AsyncMock()
|
|
session.rollback = AsyncMock()
|
|
session.close = AsyncMock()
|
|
|
|
return session
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_policy_loader():
|
|
"""Create mock policy loader for testing"""
|
|
loader = MagicMock()
|
|
|
|
# Mock policy loading
|
|
loader.load_policy = MagicMock()
|
|
loader.compile_predicates = MagicMock()
|
|
loader.validate_policy = MagicMock()
|
|
|
|
return loader
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_test_environment():
|
|
"""Set up test environment variables"""
|
|
original_env = os.environ.copy()
|
|
|
|
# Set test environment variables
|
|
os.environ.update(
|
|
{
|
|
"ENVIRONMENT": "test",
|
|
"CONFIG_DIR": "/tmp/test-config",
|
|
"NEO4J_URI": "bolt://localhost:7687",
|
|
"NEO4J_USER": "neo4j",
|
|
"NEO4J_PASSWORD": "testpass",
|
|
"POSTGRES_URL": "postgresql://postgres:postgres@localhost:5432/test_db",
|
|
"QDRANT_URL": "http://localhost:6333",
|
|
"VAULT_URL": "http://localhost:8200",
|
|
"VAULT_TOKEN": "test-token",
|
|
}
|
|
)
|
|
|
|
yield
|
|
|
|
# Restore original environment
|
|
os.environ.clear()
|
|
os.environ.update(original_env)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_evidence_data():
|
|
"""Sample evidence data for testing"""
|
|
return [
|
|
{
|
|
"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-15T10:00:00Z",
|
|
},
|
|
{
|
|
"doc_id": "DOC-P11D-001",
|
|
"kind": "P11D",
|
|
"page": 1,
|
|
"bbox": {"x": 50, "y": 100, "width": 400, "height": 60},
|
|
"ocr_confidence": 0.88,
|
|
"extract_confidence": 0.90,
|
|
"date": "2024-07-06T14:30:00Z",
|
|
},
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_citation_data():
|
|
"""Sample citation data for testing"""
|
|
return [
|
|
{
|
|
"rule_id": "UK.SA102.P60.Required",
|
|
"doc_id": "SA102-Notes-2025",
|
|
"locator": "p.3 §1.1",
|
|
"url": "https://docs.local/SA102-Notes-2025#p3s1.1",
|
|
},
|
|
{
|
|
"rule_id": "UK.SA102.P11D.Conditional",
|
|
"doc_id": "SA102-Notes-2025",
|
|
"locator": "p.5 §2.3",
|
|
"url": "https://docs.local/SA102-Notes-2025#p5s2.3",
|
|
},
|
|
]
|
|
|
|
|
|
# Pytest markers for test categorization
|
|
pytest_plugins: list[str] = []
|
|
|
|
|
|
def pytest_configure(config):
|
|
"""Configure pytest markers"""
|
|
config.addinivalue_line("markers", "unit: mark test as a unit test")
|
|
config.addinivalue_line("markers", "integration: mark test as an integration test")
|
|
config.addinivalue_line("markers", "e2e: mark test as an end-to-end test")
|
|
config.addinivalue_line("markers", "slow: mark test as slow running")
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
"""Automatically mark tests based on their location"""
|
|
for item in items:
|
|
# Mark tests based on directory structure
|
|
if "unit" in str(item.fspath):
|
|
item.add_marker(pytest.mark.unit)
|
|
elif "integration" in str(item.fspath):
|
|
item.add_marker(pytest.mark.integration)
|
|
elif "e2e" in str(item.fspath):
|
|
item.add_marker(pytest.mark.e2e)
|
|
|
|
# Mark async tests
|
|
if asyncio.iscoroutinefunction(item.function):
|
|
item.add_marker(pytest.mark.asyncio)
|