dotb_ai-platform/tests/test_medium_scoring.py
2026-09-14 09:57:12 +07:00

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