diff --git a/backend/scripts/test_user_journey.py b/backend/scripts/test_user_journey.py new file mode 100644 index 0000000..ed3feb2 --- /dev/null +++ b/backend/scripts/test_user_journey.py @@ -0,0 +1,88 @@ +"""Full user-journey flow test (mock LLM) covering the exact 'idea' flow end-to-end: +admin create group -> analyze -> personas -> trainee picks scenario -> chat -> +persona decides -> debrief readable. Verifies no 500s and debrief is user-readable. +""" +import io, os, sys, tempfile, warnings +from pathlib import Path + +warnings.filterwarnings("ignore") +BACKEND = str(Path(__file__).resolve().parents[1]) +sys.path.insert(0, BACKEND) +from app.factory import create_app +from app.config import Config + +td = tempfile.mkdtemp(); Config.DATA_DIR = Path(td) +sys.path.insert(0, BACKEND + "/scripts") +from mock_llm import MockLLM + +app = create_app(); app.extensions["llm"] = MockLLM() +C = app.test_client() + +def tok(u, p): return C.post("/api/auth/login", json={"username": u, "password": p}).get_json()["token"] + +AT = tok("admin", "1234"); AH = {"Authorization": f"Bearer {AT}"} +C.post("/api/auth/setup", headers=AH, json={"username":"admin","email":"a@b.co","password":"newpass","accepted_terms":True}) +AT = tok("admin", "newpass"); AH = {"Authorization": f"Bearer {AT}"} + +# 1. admin creates group +r = C.post("/api/groups", headers=AH, json={"product":"Point-of-Sale CRM","segment":"SME restaurants","channel":"line","language":"th"}) +assert r.status_code == 201, r.get_json() +gid = r.get_json()["group"]["id"] +print("[ok] admin created group", gid[:8]) + +# 2. analyze +r = C.post(f"/api/groups/{gid}/analyze", headers=AH) +assert r.status_code == 200, r.get_json() +g = C.get(f"/api/groups/{gid}", headers=AH).get_json()["group"] +assert g.get("status") in ("ready", "draft"), g.get("status") +print("[ok] analyze produced", g.get("status")) + +# 3. super_admin sees personas WITH secret fields (recipe), tiers populated +ps = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"] +assert ps and len(ps) >= 3 +assert "pains" in ps[0], "super_admin should see recipe fields" +print("[ok] super_admin sees", len(ps), "personas incl. recipe (pains)") + +# 4. create a trainee (user) in the same org, login +C.post("/api/admin/users", headers=AH, json={"username":"trainee","password":"pppp","role":"user"}) +TT = tok("trainee", "pppp"); TH = {"Authorization": f"Bearer {TT}"} + +# 5. trainee picks scenario and starts chat with persona 0 +pid = ps[0]["id"] +r = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=TH, json={"scenario":"social","locale":"th"}) +assert r.status_code == 200, r.get_json() +assert any(m.get("role") == "customer" for m in r.get_json()["session"]["messages"]), "social = customer opens" +print("[ok] trainee started social scenario; customer opens") + +# 6. trainee sends messages until persona decides (mock decides buy on first send) +r = C.post(f"/api/chat/{gid}/personas/{pid}/chat/send", headers=TH, json={"text":"สวัสดีครับ ช่วยแนะนำหน่อยได้ไหม"}) +assert r.status_code == 200, r.get_json() +b = r.get_json() +assert b.get("finished") is True and b.get("outcome") == "won", b +db = b.get("debrief") or {} +# 7. debrief must be user-readable: no raw nested JSON blobs leaking internals unexpectedly +assert db.get("outcome") == "won" +assert isinstance(db.get("coaching"), list) and db.get("coaching") +assert isinstance(db.get("why"), str) and db.get("why") +print("[ok] chat ended (persona decided buy); debrief readable") + +# 8. one-shot: trainee cannot start again on same persona +r = C.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=TH, json={"scenario":"social"}) +assert r.status_code in (400, 409), r.get_json() +print("[ok] one-shot enforced: cannot restart finished persona") + +# 9. admin (non-super) sees persona WITHOUT recipe (IP protection) +C.post("/api/admin/users", headers=AH, json={"username":"adm","password":"pppp","role":"admin"}) +ADM = tok("adm","pppp"); ADH = {"Authorization": f"Bearer {ADM}"} +ps2 = C.get(f"/api/groups/{gid}/personas", headers=ADH).get_json()["personas"] +assert "pains" not in ps2[0] and "tolerance" not in ps2[0], "admin must NOT see recipe" +print("[ok] admin sees personas but recipe fields stripped (IP)") + +# 10. trainee sees a minimal revealable persona (no hidden signals) +pt = C.get(f"/api/groups/{gid}/personas", headers=TH).get_json()["personas"] +pfirst = pt[0] +for hidden in ("pains","tolerance","negotiation_levers","opener","income","background"): + assert hidden not in pfirst, f"trainee should not see '{hidden}' before chat" +print("[ok] trainee sees only revealable persona (latent hidden)") + +print("ALL USER-JOURNEY FLOW TESTS PASSED")