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:
346
tests/unit/coverage/test_policy_load_and_merge.py
Normal file
346
tests/unit/coverage/test_policy_load_and_merge.py
Normal 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
|
||||
Reference in New Issue
Block a user