dotb_ai-platform/tests/test_domain_scoring.py
2026-09-25 09:55:25 +07:00

145 lines
4.8 KiB
Python

from ai_platform.lead_scoring.domain.scoring import Rule, ScoringConfiguration, score_rules
def test_caller_supplied_rules_are_deterministic_and_bounded():
snapshot = {"lead_profile": {"custom_fields": {"region": "north", "age": 22}}}
policy = ScoringConfiguration(
version="omni-7",
rules=(
Rule("region", "custom_fields.region", "eq", "north", 65),
Rule("age", "custom_fields.age", "gte", 18, 20),
Rule("miss", "custom_fields.age", "gt", 30, 40),
),
rule_weight=1.0,
ai_weight=0.0,
)
first = score_rules(snapshot, policy)
assert first == score_rules(snapshot, policy)
assert first[0] == 85
assert [item["rule_id"] for item in first[1]] == ["region", "age", "miss"]
def test_contact_recency_uses_latest_instant_across_timezones_and_message_order():
base = {
"captured_at": "2025-09-25T10:00:00Z",
"lead_profile": {"custom_fields": {}},
"conversations": [
{
"messages": [
{"sender_type": "contact", "sent_at": "2025-09-25T08:00:00+02:00"},
{"sender_type": "contact", "sent_at": "2025-09-24T09:00:00Z"},
{"sender_type": "agent", "sent_at": "2025-09-25T09:59:00Z"},
]
}
],
}
policy = ScoringConfiguration(
version="omni-9",
rules=(Rule("recency", "metrics.contact_message_recency_days", "eq", 0.17, 1),),
rule_weight=1.0,
ai_weight=0.0,
)
assert score_rules(base, policy)[1][0]["matched"]
base["conversations"][0]["messages"].reverse()
assert score_rules(base, policy)[1][0]["matched"]
def test_contact_recency_is_none_without_contact_messages():
snapshot = {
"captured_at": "2025-09-25T10:00:00Z",
"lead_profile": {"custom_fields": {}},
"conversations": [
{"messages": [{"sender_type": "agent", "sent_at": "2025-09-25T09:00:00Z"}]}
],
}
policy = ScoringConfiguration(
version="omni-10",
rules=(Rule("missing", "metrics.contact_message_recency_days", "exists", None, 1),),
rule_weight=1.0,
ai_weight=0.0,
)
assert score_rules(snapshot, policy)[1][0]["matched"] is False
def test_missing_customer_fields_do_not_match_neq_or_null_and_unknown_operators_fail():
snapshot = {"lead_profile": {"custom_fields": {}}}
policy = ScoringConfiguration(
version="omni-11",
rules=(
Rule("neq-missing", "custom_fields.region", "ne", "north", 10),
Rule("null-missing", "custom_fields.region", "eq", None, 10),
Rule("unknown", "custom_fields.region", "unsupported", "north", 10),
),
rule_weight=1.0,
ai_weight=0.0,
)
score, breakdown = score_rules(snapshot, policy)
assert score == 0
assert all(not item["matched"] and item["points"] == 0 for item in breakdown)
def test_rule_comparisons_do_not_coerce_boolean_or_string_numbers():
snapshot = {
"lead_profile": {
"custom_fields": {
"flag": True,
"age_text": "18",
"tags": [True],
"nested": {"region": "north"},
}
}
}
policy = ScoringConfiguration(
version="omni-12",
rules=(
Rule("bool-eq-int", "custom_fields.flag", "eq", 1, 20),
Rule("bool-ne-int", "custom_fields.flag", "ne", 1, 20),
Rule("bool-gte", "custom_fields.flag", "gte", 1, 20),
Rule("string-gte-number", "custom_fields.age_text", "gte", 18, 20),
Rule("contains-int-in-bool-list", "custom_fields.tags", "contains", 1, 20),
Rule("nested-field", "custom_fields.nested.region", "eq", "north", 20),
Rule("int-eq-float", "metrics.conversation_count", "eq", 0.0, 20),
),
rule_weight=1.0,
ai_weight=0.0,
)
score, breakdown = score_rules(snapshot, policy)
assert score == 40
assert [item["matched"] for item in breakdown] == [
False,
False,
False,
False,
False,
True,
True,
]
def test_rule_points_are_bounded_after_all_customer_rules():
snapshot = {"lead_profile": {"custom_fields": {"match": True}}}
positive = ScoringConfiguration(
version="omni-13",
rules=(
Rule("a", "custom_fields.match", "eq", True, 100),
Rule("b", "custom_fields.match", "eq", True, 100),
),
rule_weight=1.0,
ai_weight=0.0,
)
negative = ScoringConfiguration(
version="omni-14",
rules=(Rule("a", "custom_fields.match", "eq", True, -100),),
rule_weight=1.0,
ai_weight=0.0,
)
assert score_rules(snapshot, positive)[0] == 100
assert score_rules(snapshot, negative)[0] == 0