193 lines
6.8 KiB
Python
193 lines
6.8 KiB
Python
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from lead_scoring.config import Settings
|
|
from lead_scoring.scoring import (
|
|
EffortProfile,
|
|
FallbackPolicy,
|
|
ScoringConfig,
|
|
combine_confidence,
|
|
combine_final_score,
|
|
)
|
|
|
|
|
|
def test_scoring_config_default_weights_and_ai_fields():
|
|
config = ScoringConfig(
|
|
version="config-1",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
)
|
|
assert config.rule_weight == 0.4
|
|
assert config.ai_weight == 0.6
|
|
assert config.effort_profile == EffortProfile.MEDIUM
|
|
assert config.ai_model == "openai/gpt-4o-mini"
|
|
assert config.fallback_policy == FallbackPolicy.RULES_ONLY_REVIEW
|
|
assert config.ai_timeout_seconds == 10.0
|
|
|
|
|
|
def test_scoring_config_rejects_unknown_effort_profile():
|
|
with pytest.raises(ValidationError):
|
|
ScoringConfig.model_validate(
|
|
{
|
|
"version": "config-bad-effort",
|
|
"confidence": 0.8,
|
|
"confidence_threshold": 0.8,
|
|
"hitl_policy_version": "policy-1",
|
|
"effort_profile": "unsupported_profile",
|
|
}
|
|
)
|
|
|
|
|
|
def test_scoring_config_rejects_unknown_fallback_policy():
|
|
with pytest.raises(ValidationError):
|
|
ScoringConfig.model_validate(
|
|
{
|
|
"version": "config-bad-fallback",
|
|
"confidence": 0.8,
|
|
"confidence_threshold": 0.8,
|
|
"hitl_policy_version": "policy-1",
|
|
"fallback_policy": "unknown_fallback_policy",
|
|
}
|
|
)
|
|
|
|
|
|
def test_scoring_config_validates_weights_sum_to_one():
|
|
# Valid explicit weights
|
|
config = ScoringConfig(
|
|
version="config-2",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
rule_weight=0.3,
|
|
ai_weight=0.7,
|
|
)
|
|
assert config.rule_weight == 0.3
|
|
assert config.ai_weight == 0.7
|
|
|
|
# Invalid weights sum
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ScoringConfig(
|
|
version="config-3",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
rule_weight=0.5,
|
|
ai_weight=0.6,
|
|
)
|
|
assert "must equal 1.0" in str(exc_info.value)
|
|
|
|
|
|
def test_settings_validate_worker_lease_covers_ai_timeout():
|
|
# 2 * 10s + 5s = 25s required. worker_lease_seconds=20s must fail.
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Settings(
|
|
worker_lease_seconds=20.0,
|
|
scoring_configs=[
|
|
ScoringConfig(
|
|
version="cfg-long-ai",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
ai_timeout_seconds=10.0,
|
|
)
|
|
],
|
|
)
|
|
assert "must exceed two-pass AI timeout plus margin" in str(exc_info.value)
|
|
|
|
# Light profile (1 pass): 1 * 10s + 5s = 15s required. worker_lease_seconds=12s must fail.
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Settings(
|
|
worker_lease_seconds=12.0,
|
|
scoring_configs=[
|
|
ScoringConfig(
|
|
version="cfg-light-lease",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
effort_profile=EffortProfile.LIGHT,
|
|
ai_timeout_seconds=10.0,
|
|
)
|
|
],
|
|
)
|
|
assert "must exceed one-pass AI timeout plus margin" in str(exc_info.value)
|
|
|
|
# Heavy profile (3 passes): 3 * 10s + 5s = 35s required. worker_lease_seconds=30s must fail.
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
Settings(
|
|
worker_lease_seconds=30.0,
|
|
scoring_configs=[
|
|
ScoringConfig(
|
|
version="cfg-heavy-lease",
|
|
confidence=0.8,
|
|
confidence_threshold=0.8,
|
|
hitl_policy_version="policy-1",
|
|
effort_profile=EffortProfile.HEAVY,
|
|
ai_timeout_seconds=10.0,
|
|
)
|
|
],
|
|
)
|
|
assert "must exceed three-pass AI timeout plus margin" in str(exc_info.value)
|
|
|
|
|
|
def test_settings_reads_aihub_base_url_from_env(monkeypatch: pytest.MonkeyPatch):
|
|
monkeypatch.setenv("AIHUB_BASE_URL", "https://aihub-custom.internal.dotb.vn")
|
|
settings = Settings()
|
|
assert settings.aihub_base_url == "https://aihub-custom.internal.dotb.vn"
|
|
|
|
|
|
def test_settings_validates_aihub_base_url_shapes_and_environment():
|
|
# 1. Dev/Test accepts localhost HTTP
|
|
s_dev = Settings(environment="development", aihub_base_url="http://localhost:8080")
|
|
assert s_dev.aihub_base_url == "http://localhost:8080"
|
|
|
|
s_127 = Settings(environment="development", aihub_base_url="http://127.0.0.1:8080")
|
|
assert s_127.aihub_base_url == "http://127.0.0.1:8080"
|
|
|
|
# 2. Dev/Test rejects remote HTTP
|
|
with pytest.raises(ValidationError, match="HTTP scheme is only allowed for localhost"):
|
|
Settings(environment="development", aihub_base_url="http://untrusted-remote.com")
|
|
|
|
# 3. Production requires HTTPS
|
|
s_prod = Settings(environment="production", aihub_base_url="https://aihub.internal")
|
|
assert s_prod.aihub_base_url == "https://aihub.internal"
|
|
|
|
with pytest.raises(ValidationError, match="must use HTTPS in production"):
|
|
Settings(environment="production", aihub_base_url="http://localhost:8080")
|
|
|
|
with pytest.raises(ValidationError, match="must use HTTPS in production"):
|
|
Settings(environment="production", aihub_base_url="http://aihub.internal")
|
|
|
|
# 4. Rejects user credentials in URL
|
|
with pytest.raises(ValidationError, match="user credentials"):
|
|
Settings(aihub_base_url="https://user:pass@aihub.internal")
|
|
|
|
# 5. Rejects query parameters
|
|
with pytest.raises(ValidationError, match="query parameters"):
|
|
Settings(aihub_base_url="https://aihub.internal?env=prod")
|
|
|
|
# 6. Rejects fragments
|
|
with pytest.raises(ValidationError, match="fragments"):
|
|
Settings(aihub_base_url="https://aihub.internal#anchor")
|
|
|
|
|
|
def test_combine_final_score_deterministic_and_clamped():
|
|
# 80 * 0.4 + 90 * 0.6 = 32 + 54 = 86
|
|
assert combine_final_score(80, 90, 0.4, 0.6) == 86
|
|
|
|
# 100 * 0.5 + 0 * 0.5 = 50
|
|
assert combine_final_score(100, 0, 0.5, 0.5) == 50
|
|
|
|
# Bounds clamping [0, 100]
|
|
assert combine_final_score(0, 0, 0.4, 0.6) == 0
|
|
assert combine_final_score(100, 100, 0.4, 0.6) == 100
|
|
|
|
|
|
def test_combine_confidence_deterministic_and_clamped():
|
|
# 0.8 * 0.4 + 0.9 * 0.6 = 0.32 + 0.54 = 0.86
|
|
assert combine_confidence(0.8, 0.9, 0.4, 0.6) == 0.86
|
|
|
|
# Clamping bounds [0.0, 1.0]
|
|
assert combine_confidence(0.0, 0.0, 0.4, 0.6) == 0.0
|
|
assert combine_confidence(1.0, 1.0, 0.4, 0.6) == 1.0
|