new-site/scripts/tests/test_coupon_ab.py
justin 6fce3ec9eb trucking: A/B/C coupon price test (20/30/40% off) + SpamAssassin harness
- CAMPAIGN_COUPON_AB_PCTS="20,30,40" mints one daily code per arm; each
  carrier is bucketed by a stable sha256(email) hash so the split is even
  (~33/33/33 verified over 30k) and stable across re-sends (no arm-hopping).
- Each arm's code stores its own percent in discount_codes, so the advertised
  discount always matches what checkout applies; redemptions are countable per
  code (marker campaign-daily:<date>:<pct>).
- Empty/unset keeps legacy single-arm behavior (COUPON_PCT, legacy marker).
- coupon_attribs() now takes per-recipient pct.
- Tests: scripts/tests/test_coupon_ab.py (5 pass). SpamAssassin: both main
  campaigns (186/188) score 0.0 HAM across all 3 arms, coupon block renders
  clean; harness saved for re-runs.
2026-06-20 16:41:47 -05:00

76 lines
2.6 KiB
Python

#!/usr/bin/env python3
"""Unit tests for the trucking same-day coupon A/B/C price test.
Verifies that CAMPAIGN_COUPON_AB_PCTS produces an even, stable per-email split,
that the advertised percent always matches the arm's actual code (so the email
never promises a discount checkout won't honor), and that the off / single-arm
states behave. Pure-function tests, no DB required (psycopg2 is stubbed).
Run: CAMPAIGN_COUPON_AB_PCTS="20,30,40" python3 scripts/tests/test_coupon_ab.py
"""
from __future__ import annotations
import importlib.util
import os
import sys
import types
from collections import Counter
from pathlib import Path
os.environ.setdefault("CAMPAIGN_COUPON_AB_PCTS", "20,30,40")
# Stub the DB driver so the builder imports without a live Postgres.
sys.modules.setdefault("psycopg2", types.ModuleType("psycopg2"))
_SCRIPT = Path(__file__).resolve().parents[1] / "build_trucking_campaigns.py"
_spec = importlib.util.spec_from_file_location("btc", _SCRIPT)
btc = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(btc)
def test_arms_parsed():
assert btc.COUPON_AB_PCTS == (20, 30, 40)
def test_even_stable_and_code_matches_pct():
coupons = {20: "AAAAA", 30: "BBBBB", 40: "CCCCC"}
emails = [f"user{i}@carrier{i % 500}.com" for i in range(30000)]
counts: Counter = Counter()
for e in emails:
code, pct = btc.pick_coupon_for_email(e, coupons)
counts[pct] += 1
# Advertised percent must match the arm's real code.
assert coupons[int(pct)] == code
# Stable: same input -> same arm.
assert btc.pick_coupon_for_email(e, coupons) == (code, pct)
total = sum(counts.values())
# Each arm should be within ~2 points of an even third.
for pct in ("20", "30", "40"):
share = counts[pct] / total
assert 0.31 <= share <= 0.353, (pct, share)
def test_off_state():
assert btc.pick_coupon_for_email("x@y.com", None) == ("", "")
assert btc.pick_coupon_for_email("x@y.com", {}) == ("", "")
def test_single_arm():
assert btc.pick_coupon_for_email("a@b.com", {40: "ZZZZZ"}) == ("ZZZZZ", "40")
def test_coupon_attribs_reflects_pct():
a = btc.coupon_attribs("BBBBB", "30")
assert a["coupon_code"] == "BBBBB"
assert a["coupon_pct"] == "30"
assert btc.coupon_attribs("", "30") == {
"coupon_code": "", "coupon_pct": "", "coupon_expires": ""
}
if __name__ == "__main__":
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for fn in fns:
fn()
print(f"ok {fn.__name__}")
print(f"\nAll {len(fns)} coupon A/B tests passed.")