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 # noqa: E402 from app.main import app, category_arm_pool, contexts, conversations, merchant_action_memory, merchant_auto_replies, merchant_opt_out, suppressed # noqa: E402 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