"""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)") # 11. 'create more personas' = append (does not replace existing ones) before = len(C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]) r = C.post(f"/api/groups/{gid}/analyze?append=true", headers=AH) assert r.status_code == 200, r.get_json() after = len(r.get_json()["personas"]) assert after > before, f"append should add personas (before={before}, after={after})" print(f"[ok] append adds personas ({before} -> {after}); existing kept") # 12. delete group: trainee cannot, admin can r = C.delete(f"/api/groups/{gid}", headers=TH) assert r.status_code == 403, ("trainee must not delete group", r.status_code) print("[ok] trainee cannot delete group (403)") r = C.delete(f"/api/groups/{gid}", headers=AH) assert r.status_code == 200, r.get_json() gone = C.get(f"/api/groups/{gid}", headers=AH) assert gone.status_code == 404, "deleted group should be gone" print("[ok] admin deletes group; group no longer reachable") print("ALL USER-JOURNEY FLOW TESTS PASSED")