system-prompts-and-models-o.../dealix/tests/unit/test_customer_success.py
2026-05-01 14:03:52 +03:00

187 lines
6.2 KiB
Python

"""Smoke tests for the Customer Success layer — health, QBR, benchmarks."""
from __future__ import annotations
import pytest
from auto_client_acquisition.customer_success.benchmarks import (
MIN_COHORT_SIZE,
compare_customer,
compute_sector_benchmark,
percentile,
saudi_b2b_pulse,
)
from auto_client_acquisition.customer_success.health_score import compute_health
from auto_client_acquisition.customer_success.qbr_generator import generate_qbr
# ── Health score ──────────────────────────────────────────────────
def test_health_zero_signals_is_critical():
h = compute_health(customer_id="c1")
assert h.bucket == "critical"
assert h.overall <= 40
def test_health_strong_signals_is_healthy():
h = compute_health(
customer_id="c2",
logins_last_30d=22,
drafts_approved_last_30d=40,
replies_acted_on_last_30d=12,
demos_booked_last_30d=8,
deals_stage_progressed_last_30d=10,
paid_customers_last_30d=3,
pipeline_value_sar=500_000,
channels_enabled=4,
integrations_connected=4,
sectors_targeted=3,
total_drafts_lifetime=400,
nps=9,
)
assert h.bucket == "healthy"
assert h.overall >= 75
assert isinstance(h.upsell_candidate, bool)
def test_health_buckets_in_order():
"""Higher composite signals must produce higher overall scores."""
weak = compute_health(customer_id="w", logins_last_30d=2, drafts_approved_last_30d=1)
strong = compute_health(
customer_id="s",
logins_last_30d=20,
drafts_approved_last_30d=30,
replies_acted_on_last_30d=8,
demos_booked_last_30d=4,
deals_stage_progressed_last_30d=6,
paid_customers_last_30d=2,
)
assert strong.overall > weak.overall
def test_health_returns_drivers():
h = compute_health(customer_id="c3", logins_last_30d=15, drafts_approved_last_30d=20)
# drivers is a list[str] of human-readable driver tags
assert isinstance(h.drivers, list)
# And the 4 dimension scores are exposed as named attributes
for dim in ("engagement", "outcomes", "adoption", "sentiment"):
assert hasattr(h, dim)
# ── QBR generator ─────────────────────────────────────────────────
def test_qbr_returns_markdown():
qbr = generate_qbr(
customer_id="c1",
customer_name="Test Co.",
emails_sent=120,
emails_replied=14,
demos_booked=4,
)
md = qbr.to_markdown()
assert "Test Co." in md
assert qbr.customer_id == "c1"
assert isinstance(qbr.sections, list)
assert qbr.sections, "QBR must have at least one section"
def test_qbr_sections_have_titles():
qbr = generate_qbr(customer_id="c", customer_name="X")
# QBRSection is a dataclass with a title attribute
for s in qbr.sections:
assert isinstance(s.title, str)
# ── Benchmarks — privacy guard ────────────────────────────────────
def test_benchmarks_min_cohort_is_5():
"""Privacy: must refuse to publish stats for fewer than 5 companies."""
assert MIN_COHORT_SIZE >= 5
def test_benchmark_returns_none_below_min_cohort():
out = compute_sector_benchmark(
sector="clinics",
metric="reply_rate",
customer_values=[0.10, 0.12, 0.08], # only 3 — below min
)
assert out is None
def test_benchmark_returns_value_at_min_cohort():
out = compute_sector_benchmark(
sector="clinics",
metric="reply_rate",
customer_values=[0.05, 0.10, 0.12, 0.15, 0.20],
)
assert out is not None
assert out.cohort_size == 5
assert out.metric == "reply_rate"
assert out.p50 >= out.p25
assert out.p90 >= out.p50
# ── Percentile correctness ────────────────────────────────────────
def test_percentile_p50_matches_median():
vals = [1.0, 2.0, 3.0, 4.0, 5.0]
assert percentile(vals, 50) == 3.0
def test_percentile_p90_above_p50():
vals = [1.0, 2.0, 3.0, 4.0, 5.0]
assert percentile(vals, 90) >= percentile(vals, 50)
def test_percentile_handles_single_value():
assert percentile([3.0], 50) == 3.0
# ── compare_customer ──────────────────────────────────────────────
def test_compare_returns_position():
out = compare_customer(
customer_id="c1",
sector="clinics",
metric="reply_rate",
customer_value=0.18,
sector_values=[0.05, 0.08, 0.10, 0.12, 0.15, 0.18, 0.20],
)
assert out is not None
assert out.customer_id == "c1"
# Field is named `customer_percentile` (int 0-100)
assert 0 <= out.customer_percentile <= 100
def test_compare_below_min_cohort_returns_none():
out = compare_customer(
customer_id="c1",
sector="clinics",
metric="reply_rate",
customer_value=0.10,
sector_values=[0.05, 0.10], # only 2
)
assert out is None
# ── Saudi B2B Pulse — full report ─────────────────────────────────
def test_pulse_skips_undersized_sectors():
"""Sectors with <5 samples must NOT appear in published pulse."""
pulse = saudi_b2b_pulse(
sector_data={
"clinics": {"reply_rate": [0.10, 0.12, 0.14, 0.15, 0.18, 0.20]},
"tiny_sector": {"reply_rate": [0.10, 0.12]}, # below min — must be excluded
}
)
sector_names = {b["sector"] for b in pulse.get("benchmarks", [])}
assert "clinics" in sector_names
assert "tiny_sector" not in sector_names
def test_pulse_returns_publishable_dict():
pulse = saudi_b2b_pulse(
sector_data={
"clinics": {"reply_rate": [0.10, 0.12, 0.14, 0.15, 0.18, 0.20]},
"real_estate": {"reply_rate": [0.04, 0.06, 0.07, 0.09, 0.11]},
}
)
assert isinstance(pulse, dict)
assert "benchmarks" in pulse
assert pulse["report_name"] == "Saudi B2B Pulse"
assert pulse["min_cohort_for_publication"] >= 5