mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 15:29:36 +00:00
248 lines
8.0 KiB
Python
248 lines
8.0 KiB
Python
"""Smoke tests for compliance gate + reply classifier + targeting helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from auto_client_acquisition.email.compliance import (
|
|
ComplianceCheck,
|
|
append_opt_out_line,
|
|
check_outreach,
|
|
)
|
|
from auto_client_acquisition.email.reply_classifier import (
|
|
build_classification,
|
|
classify_rule_based,
|
|
classify_reply,
|
|
)
|
|
from auto_client_acquisition.email.daily_targeting import (
|
|
angle_for,
|
|
select_top_n_diversified,
|
|
)
|
|
|
|
|
|
# ── Compliance gate ───────────────────────────────────────────────
|
|
def test_compliance_blocks_no_email():
|
|
chk = check_outreach(to_email=None, allowed_use="business_contact_research_only")
|
|
assert chk.allowed is False
|
|
assert "no_recipient_email" in chk.blocked_reasons
|
|
|
|
|
|
def test_compliance_blocks_invalid_email():
|
|
chk = check_outreach(to_email="not-an-email", allowed_use="business_contact_research_only")
|
|
assert chk.allowed is False
|
|
assert "invalid_email_format" in chk.blocked_reasons
|
|
|
|
|
|
def test_compliance_blocks_opt_out():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me", contact_opt_out=True,
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert "contact_opt_out_true" in chk.blocked_reasons
|
|
|
|
|
|
def test_compliance_blocks_suppression():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me",
|
|
suppression_emails={"x@dealix.me"},
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert "email_suppressed" in chk.blocked_reasons
|
|
|
|
|
|
def test_compliance_blocks_high_risk():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me", risk_score=80,
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert any("risk_score_too_high" in r for r in chk.blocked_reasons)
|
|
|
|
|
|
def test_compliance_blocks_no_allowed_use():
|
|
chk = check_outreach(to_email="x@dealix.me", allowed_use=None)
|
|
assert chk.allowed is False
|
|
assert "allowed_use_missing" in chk.blocked_reasons
|
|
|
|
|
|
def test_compliance_blocks_daily_limit():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me", sent_today_count=50,
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert any("daily_limit_hit" in r for r in chk.blocked_reasons)
|
|
|
|
|
|
def test_compliance_blocks_batch_size():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me", sent_in_current_batch=10,
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert any("batch_size_hit" in r for r in chk.blocked_reasons)
|
|
|
|
|
|
def test_compliance_blocks_batch_cooldown():
|
|
chk = check_outreach(
|
|
to_email="x@dealix.me",
|
|
seconds_since_last_batch=600, # 10 min, cooldown is 90 min
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.allowed is False
|
|
assert any("batch_cooldown" in r for r in chk.blocked_reasons)
|
|
|
|
|
|
def test_compliance_personal_email_review_required():
|
|
chk = check_outreach(
|
|
to_email="x@gmail.com",
|
|
allowed_use="business_contact_research_only",
|
|
)
|
|
assert chk.requires_human_review is True
|
|
assert chk.allowed is False
|
|
assert any("personal_email_domain_review_required" in n for n in chk.notes)
|
|
|
|
|
|
def test_compliance_allows_clean_business_email():
|
|
chk = check_outreach(
|
|
to_email="x@aramco.com",
|
|
allowed_use="business_contact_research_only",
|
|
risk_score=10,
|
|
sent_today_count=5,
|
|
sent_in_current_batch=2,
|
|
seconds_since_last_batch=10000,
|
|
)
|
|
assert chk.allowed is True
|
|
assert chk.blocked_reasons == []
|
|
|
|
|
|
# ── Opt-out line appender ─────────────────────────────────────────
|
|
def test_append_opt_out_adds_line():
|
|
body = "مرحباً، تجربة Dealix"
|
|
out = append_opt_out_line(body)
|
|
assert "STOP" in out or "إيقاف" in out
|
|
|
|
|
|
def test_append_opt_out_idempotent():
|
|
body = "مرحباً، تجربة Dealix\n\n— STOP لإلغاء"
|
|
out = append_opt_out_line(body)
|
|
# Already has STOP — shouldn't double-append
|
|
assert out.count("STOP") == 1
|
|
|
|
|
|
# ── Reply classifier (rule-based, no LLM needed) ──────────────────
|
|
def test_classify_unsubscribe_arabic():
|
|
cat, conf = classify_rule_based("STOP")
|
|
assert cat == "unsubscribe"
|
|
assert conf >= 0.5
|
|
|
|
|
|
def test_classify_unsubscribe_english():
|
|
cat, conf = classify_rule_based("Please unsubscribe me")
|
|
assert cat == "unsubscribe"
|
|
|
|
|
|
def test_classify_interested():
|
|
cat, conf = classify_rule_based("نعم تجربة، نبدأ متى؟")
|
|
assert cat == "interested"
|
|
|
|
|
|
def test_classify_ask_price():
|
|
cat, conf = classify_rule_based("كم السعر؟")
|
|
assert cat == "ask_price"
|
|
|
|
|
|
def test_classify_ask_demo():
|
|
cat, conf = classify_rule_based("can we book a demo?")
|
|
assert cat == "ask_demo"
|
|
|
|
|
|
def test_classify_objection_budget():
|
|
cat, conf = classify_rule_based("غالي علينا، الميزانية محدودة")
|
|
assert cat == "objection_budget"
|
|
|
|
|
|
def test_classify_objection_ai():
|
|
cat, conf = classify_rule_based("نبي إنسان حقيقي مو روبوت")
|
|
assert cat == "objection_ai"
|
|
|
|
|
|
def test_classify_partnership():
|
|
cat, conf = classify_rule_based("نبي نكون شركاء توزيع")
|
|
assert cat == "partnership"
|
|
|
|
|
|
def test_classify_unclear_for_empty():
|
|
cat, conf = classify_rule_based("")
|
|
assert cat == "unclear"
|
|
|
|
|
|
def test_classify_already_has_crm():
|
|
cat, conf = classify_rule_based("عندنا HubSpot أصلاً، شكراً")
|
|
assert cat == "already_has_crm"
|
|
|
|
|
|
def test_build_classification_unsubscribe_auto_send_allowed():
|
|
rc = build_classification("unsubscribe", 0.95, "STOP")
|
|
# Auto-send the ack is allowed (mandatory courtesy)
|
|
assert rc.auto_send_allowed is True
|
|
assert rc.followup_days is None
|
|
|
|
|
|
def test_build_classification_angry_requires_review():
|
|
rc = build_classification("angry", 0.9, "this is spam complaint")
|
|
assert rc.requires_human_review is True
|
|
assert rc.auto_send_allowed is False
|
|
|
|
|
|
def test_build_classification_low_confidence_requires_review():
|
|
rc = build_classification("interested", 0.2, "?")
|
|
assert rc.requires_human_review is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_classify_reply_async_falls_back_to_rules():
|
|
rc = await classify_reply("STOP", prefer_llm=True)
|
|
# No LLM key in test env → falls back to rules
|
|
assert rc.category == "unsubscribe"
|
|
|
|
|
|
# ── Daily targeting helpers ───────────────────────────────────────
|
|
def test_angle_for_known_sector():
|
|
assert "عقاري" in angle_for("real_estate_developer")
|
|
assert "حفل" in angle_for("events")
|
|
assert "RFQ" in angle_for("logistics") or "شحن" in angle_for("logistics")
|
|
|
|
|
|
def test_angle_for_unknown_falls_back():
|
|
msg = angle_for("xyz_unknown")
|
|
assert "Dealix" in msg
|
|
|
|
|
|
def test_select_top_n_diversified_caps_per_sector():
|
|
candidates = [
|
|
{"id": str(i), "sector": "real_estate", "total_score": 90 - i, "data_quality_score": 50}
|
|
for i in range(20)
|
|
] + [
|
|
{"id": f"l{i}", "sector": "logistics", "total_score": 60 - i, "data_quality_score": 40}
|
|
for i in range(20)
|
|
]
|
|
chosen = select_top_n_diversified(candidates, target_count=20)
|
|
sectors = [c["sector"] for c in chosen]
|
|
# Default cap = max(20//4, 10) = 10 per sector
|
|
assert sectors.count("real_estate") <= 10
|
|
assert sectors.count("logistics") <= 10
|
|
assert len(chosen) == 20
|
|
|
|
|
|
def test_select_top_n_respects_total_count():
|
|
candidates = [
|
|
{"id": str(i), "sector": "logistics", "total_score": 100 - i, "data_quality_score": 50}
|
|
for i in range(50)
|
|
]
|
|
chosen = select_top_n_diversified(candidates, target_count=15)
|
|
# default cap = max(15//4, 10) = 10. So we can pick 10 logistics, the rest none
|
|
assert len(chosen) == 10 # bounded by cap, not target_count
|