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

This commit is contained in:
harkon
2025-10-11 08:41:36 +01:00
commit b324ff09ef
276 changed files with 55220 additions and 0 deletions

View File

@@ -0,0 +1,346 @@
"""Unit tests for policy loading and merging functionality."""
# FILE: tests/unit/coverage/test_policy_load_and_merge.py
import tempfile
from pathlib import Path
from unittest.mock import patch
import pytest
import yaml
from libs.policy import PolicyLoader
from libs.schemas import CoveragePolicy, PolicyError
# 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
class TestPolicyLoader:
"""Test policy loading and merging functionality"""
@pytest.fixture
def temp_config_dir(self):
"""Create temporary config directory with test 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}},
"document_kinds": ["P60", "P11D"],
"triggers": {
"SA102": {"any_of": ["exists(IncomeItem[type='Employment'])"]}
},
"schedules": {
"SA102": {
"evidence": [
{"id": "P60", "role": "REQUIRED", "boxes": ["SA102_b1"]}
]
}
},
"status_classifier": {
"present_verified": {"min_ocr": 0.82},
"present_unverified": {"min_ocr": 0.60},
"conflicting": {"conflict_rules": []},
"missing": {"default": True},
},
"conflict_resolution": {"precedence": ["P60"]},
"question_templates": {
"default": {
"text": "Need {evidence}",
"why": "Required for {schedule}",
}
},
"privacy": {"vector_pii_free": True, "redact_patterns": []},
}
with open(config_dir / "coverage.yaml", "w") as f:
yaml.dump(baseline_policy, f)
# Create jurisdiction overlay
jurisdiction_overlay = {
"defaults": {
"confidence_thresholds": {"ocr": 0.85} # Override threshold
},
"document_kinds": ["P60", "P11D", "P45"], # Add P45
}
with open(config_dir / "coverage.UK.2024-25.yaml", "w") as f:
yaml.dump(jurisdiction_overlay, f)
# Create tenant overlay
(config_dir / "overrides").mkdir()
tenant_overlay = {
"defaults": {"date_tolerance_days": 60} # Override tolerance
}
with open(config_dir / "overrides" / "tenant123.yaml", "w") as f:
yaml.dump(tenant_overlay, f)
yield config_dir
@pytest.fixture
def policy_loader(self, temp_config_dir):
"""Create policy loader with temp config"""
return PolicyLoader(str(temp_config_dir))
def test_load_baseline_policy(self, policy_loader, temp_config_dir):
"""Test loading baseline policy without overlays"""
policy = policy_loader.load_policy(
baseline_path=str(temp_config_dir / "coverage.yaml"),
jurisdiction="US", # No overlay exists
tax_year="2023-24", # No overlay exists
tenant_id=None,
)
assert isinstance(policy, CoveragePolicy)
assert policy.version == "1.0"
assert policy.jurisdiction == "UK"
assert policy.defaults.confidence_thresholds["ocr"] == 0.82
assert len(policy.document_kinds) == 2
def test_load_policy_with_jurisdiction_overlay(self, policy_loader):
"""Test loading policy with jurisdiction overlay applied"""
policy = policy_loader.load_policy(jurisdiction="UK", tax_year="2024-25")
# Should have jurisdiction overlay applied
assert policy.defaults.confidence_thresholds["ocr"] == 0.85 # Overridden
assert len(policy.document_kinds) == 3 # P45 added
assert "P45" in policy.document_kinds
def test_load_policy_with_tenant_overlay(self, policy_loader):
"""Test loading policy with tenant overlay applied"""
policy = policy_loader.load_policy(
jurisdiction="UK", tax_year="2024-25", tenant_id="tenant123"
)
# Should have both jurisdiction and tenant overlays
assert policy.defaults.confidence_thresholds["ocr"] == 0.85 # From jurisdiction
assert policy.defaults.date_tolerance_days == 60 # From tenant
assert len(policy.document_kinds) == 3 # From jurisdiction
def test_merge_overlays(self, policy_loader):
"""Test overlay merging logic"""
base = {"a": 1, "b": {"x": 10, "y": 20}, "c": [1, 2, 3]}
overlay1 = {
"b": {"x": 15, "z": 30}, # Merge into b, override x, add z
"d": 4, # Add new key
}
overlay2 = {
"b": {"y": 25}, # Override y in b
"c": [4, 5, 6], # Replace entire list
}
result = policy_loader.merge_overlays(base, overlay1, overlay2)
assert result["a"] == 1
assert result["b"]["x"] == 15 # From overlay1
assert result["b"]["y"] == 25 # From overlay2
assert result["b"]["z"] == 30 # From overlay1
assert result["c"] == [4, 5, 6] # From overlay2
assert result["d"] == 4 # From overlay1
def test_compile_predicates(self, policy_loader):
"""Test predicate compilation"""
policy = policy_loader.load_policy()
compiled = policy_loader.compile_predicates(policy)
assert compiled.policy == policy
assert len(compiled.compiled_predicates) > 0
assert "exists(IncomeItem[type='Employment'])" in compiled.compiled_predicates
assert compiled.hash is not None
assert len(compiled.source_files) > 0
def test_predicate_execution(self, policy_loader):
"""Test that compiled predicates are callable"""
policy = policy_loader.load_policy()
compiled = policy_loader.compile_predicates(policy)
predicate = compiled.compiled_predicates[
"exists(IncomeItem[type='Employment'])"
]
# Should be callable and return boolean
result = predicate("T-001", "2024-25")
assert isinstance(result, bool)
def test_invalid_yaml_file(self, temp_config_dir):
"""Test handling of invalid YAML file"""
# Create invalid YAML
with open(temp_config_dir / "invalid.yaml", "w") as f:
f.write("invalid: yaml: content: [")
loader = PolicyLoader(str(temp_config_dir))
with pytest.raises(PolicyError, match="Invalid YAML"):
loader._load_yaml_file(str(temp_config_dir / "invalid.yaml"))
def test_missing_file(self, temp_config_dir):
"""Test handling of missing file"""
loader = PolicyLoader(str(temp_config_dir))
with pytest.raises(PolicyError, match="Policy file not found"):
loader._load_yaml_file(str(temp_config_dir / "missing.yaml"))
def test_schema_validation_success(self, policy_loader, temp_config_dir):
"""Test successful schema validation"""
policy_dict = policy_loader._load_yaml_file(
str(temp_config_dir / "coverage.yaml")
)
# Should not raise exception
policy_loader._validate_policy(policy_dict)
def test_schema_validation_failure(self, policy_loader):
"""Test schema validation failure"""
invalid_policy = {
"version": "1.0",
# Missing required fields
}
with pytest.raises(Exception): # ValidationError from jsonschema
policy_loader._validate_policy(invalid_policy)
def test_business_rules_validation(self, policy_loader, temp_config_dir):
"""Test business rules validation"""
policy_dict = policy_loader._load_yaml_file(
str(temp_config_dir / "coverage.yaml")
)
result = policy_loader.validate_policy(policy_dict)
assert result.ok is True
assert len(result.errors) == 0
def test_business_rules_validation_failure(self, policy_loader):
"""Test business rules validation with errors"""
invalid_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}},
"document_kinds": ["P60"],
"triggers": {"SA102": {"any_of": ["test"]}},
"schedules": {
"SA102": {
"evidence": [
{
"id": "P11D", # Not in document_kinds
"role": "REQUIRED",
"boxes": ["SA102_b1"],
}
]
}
},
"status_classifier": {
"present_verified": {"min_ocr": 0.82},
"present_unverified": {"min_ocr": 0.60},
"conflicting": {"conflict_rules": []},
"missing": {"default": True},
},
"conflict_resolution": {"precedence": ["P60"]},
"question_templates": {"default": {"text": "test", "why": "test"}},
}
result = policy_loader.validate_policy(invalid_policy)
assert result.ok is False
assert len(result.errors) > 0
assert any("P11D" in error for error in result.errors)
def test_apply_feature_flags_placeholder(self, policy_loader):
"""Test feature flags application (placeholder)"""
policy_dict = {"test": "value"}
result = policy_loader.apply_feature_flags(policy_dict)
# Currently just returns unchanged
assert result == policy_dict
@patch("libs.policy.utils.get_policy_loader")
def test_convenience_functions(self, mock_get_loader, policy_loader):
"""Test convenience functions"""
# Create a valid mock policy for testing
from unittest.mock import MagicMock
from libs.schemas import (
ConflictRules,
CoveragePolicy,
Defaults,
Privacy,
QuestionTemplates,
StatusClassifier,
StatusClassifierConfig,
TaxYearBoundary,
)
mock_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"],
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(),
missing=StatusClassifier(),
),
triggers={},
conflict_resolution=ConflictRules(precedence=["P60"]),
question_templates=QuestionTemplates(
default={"text": "test", "why": "test"}
),
privacy=Privacy(vector_pii_free=True, redact_patterns=[]),
)
# Mock the policy loader to return our test policy
from datetime import datetime
from libs.schemas import CompiledCoveragePolicy
mock_compiled_policy = CompiledCoveragePolicy(
policy=mock_policy,
compiled_predicates={},
compiled_at=datetime.now(),
hash="test-hash",
source_files=["test.yaml"],
)
mock_loader = MagicMock()
mock_loader.load_policy.return_value = mock_policy
mock_loader.merge_overlays.side_effect = lambda base, *overlays: {
**base,
**{k: v for overlay in overlays for k, v in overlay.items()},
}
mock_loader.compile_predicates.return_value = mock_compiled_policy
mock_get_loader.return_value = mock_loader
from libs.policy import compile_predicates, load_policy, merge_overlays
# Test load_policy - use the mock policy directly since we're testing the convenience function
policy = load_policy()
assert isinstance(policy, CoveragePolicy)
assert policy.version == "1.0"
# Test merge_overlays
base = {"a": 1}
overlay = {"b": 2}
merged = merge_overlays(base, overlay)
assert merged == {"a": 1, "b": 2}
# Test compile_predicates
compiled = compile_predicates(policy)
assert compiled.policy == policy