mokshak's picture
Generalize merchant asset bridge
6ef36d5 verified
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