| from __future__ import annotations
|
|
|
| import json
|
| from pathlib import Path
|
| import sys
|
|
|
| from fastapi.testclient import TestClient
|
|
|
| ROOT = Path(__file__).resolve().parents[1]
|
| sys.path.insert(0, str(ROOT))
|
|
|
| from app.decision_engine import build_candidates, build_merchant_car, case_similarity, constitutional_violations, extract_evidence, merchant_asset_bridge_line, retrieve_similar_case, score_map, select_cialdini_principle |
| from app.main import app, category_arm_pool, contexts, conversations, merchant_action_memory, merchant_auto_replies, merchant_opt_out, suppressed
|
|
|
|
|
| def load_seed(name: str, key: str) -> list[dict]:
|
| return json.loads((ROOT / "dataset" / name).read_text(encoding="utf-8"))[key]
|
|
|
|
|
| def load_category(slug: str) -> dict:
|
| return json.loads((ROOT / "dataset" / "categories" / f"{slug}.json").read_text(encoding="utf-8"))
|
|
|
|
|
| def reset_state() -> None:
|
| contexts.clear()
|
| conversations.clear()
|
| suppressed.clear()
|
| merchant_opt_out.clear()
|
| merchant_auto_replies.clear()
|
| merchant_action_memory.clear()
|
| category_arm_pool.clear()
|
|
|
|
|
| def push(client: TestClient, scope: str, context_id: str, payload: dict, version: int = 1):
|
| return client.post(
|
| "/v1/context",
|
| json={"scope": scope, "context_id": context_id, "version": version, "payload": payload, "delivered_at": "2026-04-26T10:00:00Z"},
|
| )
|
|
|
|
|
| def test_health_and_context_idempotency():
|
| reset_state()
|
| client = TestClient(app)
|
| assert client.get("/v1/healthz").json()["status"] == "ok"
|
| cat = load_category("dentists")
|
| resp = push(client, "category", "dentists", cat)
|
| assert resp.status_code == 200
|
| assert resp.json()["accepted"] is True
|
| same = push(client, "category", "dentists", cat)
|
| assert same.status_code == 200
|
| assert same.json()["idempotent"] is True
|
| stale = push(client, "category", "dentists", cat, version=0)
|
| assert stale.status_code == 409
|
|
|
|
|
| def test_tick_creates_grounded_research_action():
|
| reset_state()
|
| client = TestClient(app)
|
| merchants = load_seed("merchants_seed.json", "merchants")
|
| triggers = load_seed("triggers_seed.json", "triggers")
|
| merchant = merchants[0]
|
| trigger = triggers[0]
|
| push(client, "category", "dentists", load_category("dentists"))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
|
|
| resp = client.post("/v1/tick", json={"now": "2026-04-26T10:30:00Z", "available_triggers": [trigger["id"]]})
|
| assert resp.status_code == 200
|
| actions = resp.json()["actions"]
|
| assert len(actions) == 1
|
| body = actions[0]["body"]
|
| assert "JIDA" in body
|
| assert "124" in body
|
| assert actions[0]["send_as"] == "vera"
|
| assert actions[0]["suppression_key"] == trigger["suppression_key"]
|
|
|
|
|
| def test_customer_consent_and_send_as():
|
| reset_state()
|
| client = TestClient(app)
|
| merchants = load_seed("merchants_seed.json", "merchants")
|
| customers = load_seed("customers_seed.json", "customers")
|
| triggers = load_seed("triggers_seed.json", "triggers")
|
| merchant = merchants[0]
|
| customer = customers[0]
|
| trigger = triggers[2]
|
| push(client, "category", "dentists", load_category("dentists"))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "customer", customer["customer_id"], customer)
|
| push(client, "trigger", trigger["id"], trigger)
|
|
|
| resp = client.post("/v1/tick", json={"now": "2026-04-26T11:00:00Z", "available_triggers": [trigger["id"]]})
|
| action = resp.json()["actions"][0]
|
| assert action["send_as"] == "merchant_on_behalf"
|
| assert action["customer_id"] == customer["customer_id"]
|
| assert "Priya" in action["body"]
|
| assert "Wed 5 Nov" in action["body"]
|
|
|
|
|
| def test_reply_replay_behaviors():
|
| reset_state()
|
| client = TestClient(app)
|
| auto = "Thank you for contacting us! Our team will respond shortly."
|
| first = client.post("/v1/reply", json={"conversation_id": "conv_auto", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 2}).json()
|
| assert first["action"] == "wait"
|
| second = client.post("/v1/reply", json={"conversation_id": "conv_auto", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 3}).json()
|
| assert second["action"] == "wait"
|
| third = client.post("/v1/reply", json={"conversation_id": "conv_auto", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 4}).json()
|
| assert third["action"] == "end"
|
|
|
| intent = client.post("/v1/reply", json={"conversation_id": "conv_intent", "merchant_id": "m1", "from_role": "merchant", "message": "Ok lets do it. Whats next?", "turn_number": 2}).json()
|
| assert intent["action"] == "send"
|
| assert "preparing" in intent["body"]
|
| assert intent["cta"] == "none"
|
|
|
| hostile = client.post("/v1/reply", json={"conversation_id": "conv_hostile", "merchant_id": "m1", "from_role": "merchant", "message": "Stop messaging me. This is useless spam.", "turn_number": 2}).json()
|
| assert hostile["action"] == "end"
|
|
|
| plain_stop = client.post("/v1/reply", json={"conversation_id": "conv_plain_stop", "merchant_id": "m1", "from_role": "merchant", "message": "STOP", "turn_number": 2}).json()
|
| assert plain_stop["action"] == "end"
|
|
|
| offtopic = client.post("/v1/reply", json={"conversation_id": "conv_offtopic", "merchant_id": "m1", "from_role": "merchant", "message": "What is the cricket score?", "turn_number": 2}).json()
|
| assert offtopic["action"] == "send"
|
| assert "outside" in offtopic["body"].lower()
|
|
|
|
|
| def test_ended_conversation_never_sends_again_and_omitted_merchant_optout():
|
| reset_state()
|
| client = TestClient(app)
|
| conversations["conv_done"] = {"merchant_id": "m_done", "ended": True, "turns": []}
|
| resp = client.post("/v1/reply", json={"conversation_id": "conv_done", "from_role": "merchant", "message": "hello", "turn_number": 5}).json()
|
| assert resp["action"] == "end"
|
|
|
| conversations["conv_stop"] = {"merchant_id": "m_stop", "ended": False, "turns": []}
|
| stop = client.post("/v1/reply", json={"conversation_id": "conv_stop", "from_role": "merchant", "message": "Stop messaging me", "turn_number": 2}).json()
|
| assert stop["action"] == "end"
|
| assert "m_stop" in merchant_opt_out
|
|
|
|
|
| def test_validation_errors_are_challenge_style_400():
|
| reset_state()
|
| client = TestClient(app)
|
| resp = client.post("/v1/context", json={"scope": "category"})
|
| assert resp.status_code == 400
|
| body = resp.json()
|
| assert body["accepted"] is False
|
| assert body["reason"] == "malformed"
|
|
|
|
|
| def test_merchant_level_auto_reply_tracking_across_conversations():
|
| reset_state()
|
| client = TestClient(app)
|
| auto = "Thank you for contacting us! Our team will respond shortly."
|
| assert client.post("/v1/reply", json={"conversation_id": "conv_auto_1", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 2}).json()["action"] == "wait"
|
| assert client.post("/v1/reply", json={"conversation_id": "conv_auto_2", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 3}).json()["action"] == "wait"
|
| assert client.post("/v1/reply", json={"conversation_id": "conv_auto_3", "merchant_id": "m1", "from_role": "merchant", "message": auto, "turn_number": 4}).json()["action"] == "end"
|
|
|
|
|
| def test_placeholder_triggers_do_not_leak_missing_fields():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[5]
|
| trigger = {
|
| "id": "trg_placeholder_competitor",
|
| "scope": "merchant",
|
| "kind": "competitor_opened",
|
| "source": "external",
|
| "merchant_id": merchant["merchant_id"],
|
| "customer_id": None,
|
| "payload": {"placeholder": True, "metric_or_topic": "competitor_opened"},
|
| "urgency": 2,
|
| "suppression_key": "competitor:placeholder",
|
| "expires_at": "2026-06-30T00:00:00Z",
|
| }
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
| action = client.post("/v1/tick", json={"now": "2026-04-26T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| assert "None" not in action["body"]
|
| assert "the available context" not in action["body"]
|
| assert action["cta"] == "binary_yes_no"
|
|
|
|
|
| def test_customer_without_matching_consent_routes_to_merchant_not_dead_line():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[7]
|
| customer = load_seed("customers_seed.json", "customers")[11]
|
| trigger = {
|
| "id": "trg_recall_no_scope",
|
| "scope": "customer",
|
| "kind": "recall_due",
|
| "source": "internal",
|
| "merchant_id": merchant["merchant_id"],
|
| "customer_id": customer["customer_id"],
|
| "payload": {"placeholder": True, "metric_or_topic": "recall_due"},
|
| "urgency": 2,
|
| "suppression_key": "recall:no_scope",
|
| "expires_at": "2026-06-30T00:00:00Z",
|
| }
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "customer", customer["customer_id"], customer)
|
| push(client, "trigger", trigger["id"], trigger)
|
| action = client.post("/v1/tick", json={"now": "2026-04-26T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| assert action["send_as"] == "vera"
|
| assert action["cta"] == "binary_yes_no"
|
| assert "I will not send" not in action["body"]
|
|
|
|
|
| def test_decision_engine_scores_case_anchor_shapes():
|
| merchants = load_seed("merchants_seed.json", "merchants")
|
| customers = load_seed("customers_seed.json", "customers")
|
| triggers = load_seed("triggers_seed.json", "triggers")
|
| cases = [
|
| (merchants[0], triggers[0], None),
|
| (merchants[0], triggers[2], customers[0]),
|
| (merchants[4], triggers[9], None),
|
| (merchants[6], triggers[13], None),
|
| (merchants[8], triggers[17], None),
|
| (merchants[8], triggers[18], customers[12]),
|
| ]
|
| for merchant, trigger, customer in cases:
|
| category = load_category(merchant["category_slug"])
|
| evidence = extract_evidence(category, merchant, trigger, customer)
|
| candidates = build_candidates(category, merchant, trigger, customer, evidence)
|
| assert candidates
|
| best = max(candidates, key=lambda c: c.total_score)
|
| assert best.total_score >= 36, (trigger["id"], best.total_score, best.body, best.rubric_scores)
|
| assert any(ch.isdigit() for ch in best.body)
|
| assert trigger["kind"].replace("_", " ") in best.body.lower() or any(e.source.startswith("trigger") for e in best.evidence)
|
|
|
|
|
| def test_full_expanded_dataset_proxy_has_no_weak_outputs():
|
| import subprocess
|
| import sys as _sys
|
| subprocess.run([_sys.executable, "dataset/generate_dataset.py", "--seed-dir", "dataset", "--out", "expanded"], cwd=ROOT, check=True, capture_output=True)
|
| result = subprocess.run([_sys.executable, "scripts/score_proxy.py", "34"], cwd=ROOT, text=True, capture_output=True)
|
| assert result.returncode == 0, result.stdout + result.stderr
|
|
|
|
|
| def test_car_map_jitai_and_best_of_n_debug_fields():
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| category = load_category(merchant["category_slug"])
|
| car = build_merchant_car(category, merchant, trigger)
|
| assert car.merchant_name != "unknown"
|
| assert car.category == "dentists"
|
| assert all(value is not None for value in car.summary().values())
|
|
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| assert len(candidates) >= 3
|
| best = max(candidates, key=lambda c: c.total_score)
|
| assert best.decision_plan if hasattr(best, "decision_plan") else True
|
| assert best.car_summary["category"] == "dentists"
|
| assert {"severity", "receptivity", "intervention_fit"} <= set(best.jitai_scores)
|
| assert {"motivation", "ability", "prompt"} <= set(best.map_scores)
|
| assert best.frame in {"loss_frame", "gain_frame", "certainty_frame", "social_proof", "professional_value", "effort_externalization"}
|
|
|
|
|
| def test_bmap_penalizes_high_friction_cta():
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| category = load_category(merchant["category_slug"])
|
| car = build_merchant_car(category, merchant, trigger)
|
| easy = score_map(car, trigger, "JIDA says 124 patients are relevant. Reply YES and I will draft it.", "binary_yes_no", "professional_value", [])
|
| hard = score_map(car, trigger, "JIDA says 124 patients are relevant. Please call, log in, choose a campaign, upload a file, and configure delivery.", "open_ended", "professional_value", [])
|
| assert easy["ability"] > hard["ability"]
|
|
|
|
|
| def test_frames_follow_trigger_shape_and_action_memory_changes_plan():
|
| merchant = load_seed("merchants_seed.json", "merchants")[1]
|
| category = load_category(merchant["category_slug"])
|
| dip = load_seed("triggers_seed.json", "triggers")[3]
|
| spike = {**dip, "id": "trg_spike_test", "kind": "perf_spike", "payload": {"metric": "calls", "delta_pct": 0.18}, "urgency": 2}
|
| dip_candidates = build_candidates(category, merchant, dip, None, extract_evidence(category, merchant, dip))
|
| spike_candidates = build_candidates(category, merchant, spike, None, extract_evidence(category, merchant, spike))
|
| assert any(c.frame == "loss_frame" for c in dip_candidates)
|
| assert any(c.frame == "gain_frame" for c in spike_candidates)
|
|
|
| remembered = {**merchant, "__vera_memory": {"last_action_type": "recovery_nudge", "last_response_intent": "auto_reply", "repeated_action_count": 3, "no_reply_count": 2}}
|
| car = build_merchant_car(category, remembered, dip)
|
| evidence = extract_evidence(category, remembered, dip, None, car)
|
| candidates = build_candidates(category, remembered, dip, None, evidence, car)
|
| assert all(c.jitai_scores["receptivity"] <= 4 for c in candidates)
|
|
|
|
|
| def test_openrouter_calibration_skips_without_key(monkeypatch):
|
| import subprocess
|
| import sys as _sys
|
| monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
|
| result = subprocess.run([_sys.executable, "scripts/geval_calibrate.py"], cwd=ROOT, text=True, capture_output=True)
|
| assert result.returncode == 0
|
| assert "skipped" in result.stdout.lower()
|
|
|
|
|
| def test_cialdini_constitution_and_tot_debug_fields():
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| category = load_category(merchant["category_slug"])
|
| car = build_merchant_car(category, merchant, trigger)
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| principle = select_cialdini_principle(car, trigger, evidence, "professional_value")
|
| assert principle in {"authority", "social_proof", "liking", "reciprocity", "scarcity", "commitment"}
|
| bad = "Dear valued partner, want to increase sales? Contact us?"
|
| violations = constitutional_violations(bad, car, trigger, "open_ended")
|
| assert "generic_or_corporate_copy" in violations
|
|
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| best = max(candidates, key=lambda c: c.total_score)
|
| assert best.thought_frames
|
| assert best.persuasion_principle
|
| assert best.reference_key.startswith("dentists:")
|
| assert not best.constitutional_violations
|
|
|
|
|
| def test_category_empirical_prior_flows_into_car():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
| first = client.post("/v1/tick", json={"now": "2026-04-26T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| client.post("/v1/reply", json={"conversation_id": first["conversation_id"], "merchant_id": merchant["merchant_id"], "from_role": "merchant", "message": "yes go ahead", "turn_number": 2})
|
| assert category_arm_pool[merchant["category_slug"]]
|
| next_trigger = {**trigger, "id": "trg_research_next", "suppression_key": "research:next"}
|
| push(client, "trigger", next_trigger["id"], next_trigger, version=1)
|
| second = client.post("/v1/tick", json={"now": "2026-04-26T10:05:00Z", "available_triggers": [next_trigger["id"]]}).json()["actions"][0]
|
| priors = second["decision_plan"]["car_summary"]["category_arm_priors"]
|
| assert priors
|
|
|
|
|
| def test_sparse_context_fallback_stays_specific_and_safe():
|
| category = load_category("restaurants")
|
| merchant = {
|
| "merchant_id": "m_sparse_restaurant",
|
| "category_slug": "restaurants",
|
| "identity": {"name": "Asha Cafe", "owner_first_name": "Asha", "locality": "Indiranagar"},
|
| "performance": {},
|
| "offers": [],
|
| "customer_aggregate": {},
|
| "signals": [],
|
| "conversation_history": [],
|
| }
|
| trigger = {
|
| "id": "trg_sparse_reactivation",
|
| "scope": "merchant",
|
| "kind": "merchant_inactive",
|
| "source": "internal",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {"days_inactive": 14},
|
| "urgency": 1,
|
| "expires_at": "2026-06-30T00:00:00Z",
|
| }
|
| car = build_merchant_car(category, merchant, trigger)
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| assert len(candidates) == 1
|
| body = candidates[0].body
|
| assert "Asha" in body
|
| assert "Indiranagar" in body
|
| assert "increase sales" not in body.lower()
|
| assert "sparse_context_floor" in candidates[0].risk_flags
|
|
|
|
|
| def test_broad_suppression_keys_are_made_unique():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[4]
|
| category = load_category(merchant["category_slug"])
|
| push(client, "category", merchant["category_slug"], category)
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| t1 = {"id": "trg_broad_1", "scope": "merchant", "kind": "curious_ask_due", "source": "internal", "merchant_id": merchant["merchant_id"], "payload": {"topic": "lunch"}, "urgency": 2, "suppression_key": "curious_ask_due"}
|
| t2 = {"id": "trg_broad_2", "scope": "merchant", "kind": "curious_ask_due", "source": "internal", "merchant_id": merchant["merchant_id"], "payload": {"topic": "dinner"}, "urgency": 2, "suppression_key": "curious_ask_due"}
|
| push(client, "trigger", t1["id"], t1)
|
| push(client, "trigger", t2["id"], t2)
|
| actions = client.post("/v1/tick", json={"now": "2026-05-02T10:00:00Z", "available_triggers": [t1["id"], t2["id"]]}).json()["actions"]
|
| assert len(actions) == 2
|
| assert actions[0]["suppression_key"] != actions[1]["suppression_key"]
|
|
|
|
|
| def test_context_updates_do_not_wipe_reply_memory():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
| first = client.post("/v1/tick", json={"now": "2026-04-26T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| client.post("/v1/reply", json={"conversation_id": first["conversation_id"], "merchant_id": merchant["merchant_id"], "from_role": "merchant", "message": "yes go ahead"})
|
| push(client, "merchant", merchant["merchant_id"], {**merchant, "signals": ["fresh update"]}, version=2)
|
| assert merchant_action_memory[merchant["merchant_id"]]["last_response_intent"] == "commitment"
|
|
|
|
|
| def test_pharmacy_without_consent_routes_to_merchant_and_avoids_medical_dispatch():
|
| merchant = load_seed("merchants_seed.json", "merchants")[8]
|
| customer = {**load_seed("customers_seed.json", "customers")[12], "consent": {"scope": []}}
|
| trigger = load_seed("triggers_seed.json", "triggers")[18]
|
| category = load_category(merchant["category_slug"])
|
| car = build_merchant_car(category, merchant, trigger, customer)
|
| evidence = extract_evidence(category, merchant, trigger, customer, car)
|
| candidates = build_candidates(category, merchant, trigger, customer, evidence, car)
|
| best = max(candidates, key=lambda c: c.total_score)
|
| assert best.send_as == "vera"
|
| assert "consent-safe" in best.body
|
| assert "dispatch" not in best.body.lower()
|
| assert "pharmacy_consent_or_medical_advice_risk" not in best.constitutional_violations
|
|
|
|
|
| def test_unseen_ops_trigger_uses_payload_without_known_template():
|
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[4]
|
| trigger = {
|
| "id": "trg_unseen_delivery_delay",
|
| "scope": "merchant",
|
| "kind": "delivery_delay_spike",
|
| "source": "review_digest",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {
|
| "theme": "delivery_late",
|
| "occurrences_30d": 6,
|
| "avg_delay_minutes": 42,
|
| "metric": "late delivery complaints",
|
| },
|
| "urgency": 3,
|
| "suppression_key": "delivery-delay:unseen",
|
| "expires_at": "2026-06-30T00:00:00Z",
|
| }
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
| action = client.post("/v1/tick", json={"now": "2026-05-02T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| body = action["body"].lower()
|
| assert "42" in body
|
| assert "delivery late" in body or "late delivery" in body
|
| assert "prep/rider handoff" in body
|
| assert "increase sales" not in body
|
|
|
|
|
| def test_missing_digest_id_retrieves_nearest_category_fact_for_research():
|
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| category = load_category("dentists")
|
| trigger = {
|
| "id": "trg_unseen_aligner_research",
|
| "scope": "merchant",
|
| "kind": "research_digest",
|
| "source": "external_digest",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {
|
| "top_item_id": "d_not_in_seed",
|
| "topic": "clear aligner consultations",
|
| "metric_or_topic": "clear aligners near me",
|
| },
|
| "urgency": 2,
|
| "suppression_key": "research:unseen-aligner",
|
| }
|
| car = build_merchant_car(category, merchant, trigger)
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| assert fact_value_for_test(evidence, "retrieved_digest_title")
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| body = max(candidates, key=lambda c: c.total_score).body.lower()
|
| assert "aligner" in body
|
| assert "category match" in body or "practo" in body
|
| assert "d_not_in_seed" not in body
|
|
|
|
|
| def test_cbr_similarity_adapts_familiar_trigger_with_shifted_merchant_state():
|
| merchant = {**load_seed("merchants_seed.json", "merchants")[4], "offers": []}
|
| category = load_category(merchant["category_slug"])
|
| trigger = {
|
| "id": "trg_review_theme_no_offer",
|
| "scope": "merchant",
|
| "kind": "review_theme_emerged",
|
| "source": "review_digest",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {
|
| "theme": "delivery_late",
|
| "occurrences_30d": 5,
|
| "avg_delay_minutes": 39,
|
| "common_quote": "late again",
|
| },
|
| "urgency": 3,
|
| "suppression_key": "review:no-offer",
|
| }
|
| car = build_merchant_car(category, merchant, trigger)
|
| match = retrieve_similar_case(car, trigger)
|
| assert match and match[0] >= 0.60
|
| assert case_similarity(car, trigger, match[1]) == match[0]
|
|
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| body = max(candidates, key=lambda c: c.total_score).body.lower()
|
| assert "kitchen-dispatch problem" in body
|
| assert "39" in body
|
| assert "increase sales" not in body
|
|
|
|
|
| def test_property_based_compulsion_fires_without_exact_trigger_name():
|
| merchant = load_seed("merchants_seed.json", "merchants")[6]
|
| category = load_category(merchant["category_slug"])
|
| trigger = {
|
| "id": "trg_restart_window_custom",
|
| "scope": "merchant",
|
| "kind": "restart_window_custom",
|
| "source": "ops_calendar",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {
|
| "available_slots": [{"label": "Mon 7 PM"}, {"label": "Wed 7 PM"}],
|
| "active_members_at_risk": 28,
|
| "metric": "member restarts",
|
| },
|
| "urgency": 3,
|
| "suppression_key": "restart-window:custom",
|
| }
|
| car = build_merchant_car(category, merchant, trigger)
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| candidates = build_candidates(category, merchant, trigger, None, evidence, car)
|
| best = max(candidates, key=lambda c: c.total_score)
|
| assert best.persuasion_principle == "scarcity"
|
| assert best.map_scores["prompt"] >= 8
|
| assert "mon 7 pm" in best.body.lower() or "wed 7 pm" in best.body.lower()
|
|
|
|
|
| def test_final_engagement_pass_adds_command_cta_line_break_and_peer_norm(): |
| reset_state()
|
| client = TestClient(app)
|
| merchant = load_seed("merchants_seed.json", "merchants")[4]
|
| trigger = {
|
| "id": "trg_unseen_delivery_delay_engagement",
|
| "scope": "merchant",
|
| "kind": "delivery_delay_spike",
|
| "source": "review_digest",
|
| "merchant_id": merchant["merchant_id"],
|
| "payload": {
|
| "theme": "delivery_late",
|
| "occurrences_30d": 6,
|
| "avg_delay_minutes": 42,
|
| "metric": "late delivery complaints",
|
| },
|
| "urgency": 3,
|
| "suppression_key": "delivery-delay:engagement",
|
| }
|
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"]))
|
| push(client, "merchant", merchant["merchant_id"], merchant)
|
| push(client, "trigger", trigger["id"], trigger)
|
| action = client.post("/v1/tick", json={"now": "2026-05-02T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]
|
| body = action["body"]
|
| assert "\n\n" in body
|
| assert "Want me" not in body
|
| assert "Should I" not in body
|
| assert "Reply YES" in body |
| assert "Restaurants in" in body |
|
|
|
|
| def test_active_offer_bridges_into_unseen_timing_trigger_without_duplication(): |
| reset_state() |
| client = TestClient(app) |
| merchant = load_seed("merchants_seed.json", "merchants")[4] |
| trigger = { |
| "id": "trg_unseen_matchday_asset_bridge", |
| "scope": "merchant", |
| "kind": "city_event_window", |
| "source": "local_calendar", |
| "merchant_id": merchant["merchant_id"], |
| "payload": { |
| "event": "Sunday derby screening", |
| "days_until": 3, |
| "views_30d": 780, |
| "calls_30d": 41, |
| "metric": "matchday demand", |
| }, |
| "urgency": 3, |
| "suppression_key": "city-event:asset-bridge", |
| } |
| push(client, "category", merchant["category_slug"], load_category(merchant["category_slug"])) |
| push(client, "merchant", merchant["merchant_id"], merchant) |
| push(client, "trigger", trigger["id"], trigger) |
| body = client.post("/v1/tick", json={"now": "2026-05-02T10:00:00Z", "available_triggers": [trigger["id"]]}).json()["actions"][0]["body"] |
| offer = merchant["offers"][0]["title"] |
| assert offer in body |
| assert body.count(offer) == 1 |
|
|
| car = build_merchant_car(load_category(merchant["category_slug"]), merchant, trigger) |
| evidence = extract_evidence(load_category(merchant["category_slug"]), merchant, trigger, None, car) |
| bridge = merchant_asset_bridge_line(car, trigger, evidence, "Metric moved 18%. Reply YES and I will draft it.") |
| assert offer in bridge |
| assert "merchant-owned hook" in bridge |
| assert merchant_asset_bridge_line(car, trigger, evidence, body) == "" |
|
|
|
|
| def test_rationale_reads_like_decision_justification(): |
| merchant = load_seed("merchants_seed.json", "merchants")[0]
|
| category = load_category(merchant["category_slug"])
|
| trigger = load_seed("triggers_seed.json", "triggers")[0]
|
| car = build_merchant_car(category, merchant, trigger)
|
| evidence = extract_evidence(category, merchant, trigger, None, car)
|
| best = max(build_candidates(category, merchant, trigger, None, evidence, car), key=lambda c: c.total_score)
|
| assert best.rationale.startswith("Trigger:")
|
| assert "Frame:" in best.rationale
|
| assert "Receptivity:" in best.rationale
|
| assert "Suppression:" in best.rationale
|
| assert "Selected signal" not in best.rationale
|
|
|
|
|
| def fact_value_for_test(evidence, label: str) -> str:
|
| return next((item.value for item in evidence if item.label == label), "")
|
|
|
|
|
| def test_teardown_clears_state():
|
| reset_state()
|
| client = TestClient(app)
|
| push(client, "category", "dentists", load_category("dentists"))
|
| assert contexts
|
| resp = client.post("/v1/teardown")
|
| assert resp.json()["cleared"] is True
|
| assert not contexts
|
|
|