"""Full E2E test with a mock LLM: analyze → personas → chat → debrief → board/analytics.""" import os import sys import tempfile import warnings from pathlib import Path warnings.filterwarnings("ignore", message="The HMAC key is") sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # for mock_llm tempdir = tempfile.mkdtemp(prefix="st_e2e_") os.environ["DATA_DIR"] = tempdir os.environ["JWT_SECRET"] = "test-secret-key-0123456789abcdef" from mock_llm import MockLLM # noqa: E402 from app.factory import create_app # noqa: E402 from app.config import Config # noqa: E402 Config.DATA_DIR = Path(tempdir) Config.LLM_API_KEY = "" Config.LLM_BASE_URL = "" def main(): app = create_app() app.extensions["llm"] = MockLLM() client = app.test_client() # admin login r = client.post("/api/auth/login", json={"email": "admin@salestrainer.local", "password": "admin123"}) AT = r.get_json()["token"] AH = {"Authorization": f"Bearer {AT}"} # create group r = client.post("/api/groups", json={ "product": "Cloud POS for small restaurants", "segment": "SME restaurants", "channel": "line", "language": "th"}, headers=AH) assert r.status_code == 201, r.get_json() gid = r.get_json()["group"]["id"] # analyze -> sales kit + 15 personas r = client.post(f"/api/groups/{gid}/analyze", headers=AH) assert r.status_code == 200, r.get_json() body = r.get_json() assert body["sales_kit"]["productName"] == "CloudPOS", body["sales_kit"] personas = body["personas"] assert len(personas) == 15, f"expected 15 personas, got {len(personas)}" tiers = {} for p in personas: tiers.setdefault(p["tier"], 0) tiers[p["tier"]] += 1 assert tiers == {"A": 5, "B": 5, "C": 5}, tiers # wrong_text special in tier C assert any(p["tier"] == "C" and p["special"] == "wrong_text" for p in personas), "no wrong_text persona" print(f"[ok] analyze -> sales kit + 15 personas (tiers {tiers}), wrong_text present") # create a trainee client.post("/api/admin/users", json={ "name": "Trainee", "email": "t@x.com", "password": "pass123", "role": "user"}, headers=AH) r = client.post("/api/auth/login", json={"email": "t@x.com", "password": "pass123"}) UT = r.get_json()["token"] UH = {"Authorization": f"Bearer {UT}"} # trainee sees group + personas but revealable-only (no pain/income) r = client.get(f"/api/groups/{gid}/personas", headers=UH) assert r.status_code == 200 plist = r.get_json()["personas"] assert len(plist) == 15 first = plist[1] assert "pains" not in first and "income" not in first, "latent fields leaked!" assert "profession" in first and "initiation_mode" in first print("[ok] trainee sees revealable-only persona fields (latent hidden)") # pick a customer-initiated persona -> start session (customer opens) cust = next(p for p in personas if p["initiation_mode"] == "customer") pid = cust["id"] r = client.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=UH) assert r.status_code == 200, r.get_json() session = r.get_json()["session"] assert session["status"] == "active" assert len(session["messages"]) >= 1 and session["messages"][0]["role"] == "customer", "customer should open" print("[ok] customer-initiated session starts with customer opener") # send messages r = client.post(f"/api/chat/{gid}/personas/{pid}/chat/send", json={"text": "Hi, I run a small noodle shop. Tell me about pricing."}, headers=UH) assert r.status_code == 200, r.get_json() assert r.get_json()["reply"] print("[ok] send message -> persona replies") # finish -> debrief reveals latent + outcome won r = client.post(f"/api/chat/{gid}/personas/{pid}/chat/finish", headers=UH) assert r.status_code == 200, r.get_json() debrief = r.get_json()["debrief"] assert debrief["outcome"] == "won" assert "revealed_persona" in debrief and "pains" in debrief["revealed_persona"] print("[ok] finish -> debrief with latent reveal + outcome") # ONE-SHOT: cannot start again on same persona r = client.post(f"/api/chat/{gid}/personas/{pid}/chat/start", headers=UH) assert r.status_code == 400, r.get_json() print("[ok] one-shot enforced (cannot re-chat same persona)") # seller-initiated persona -> session starts WITHOUT opener (task for seller) sel = next(p for p in personas if p["initiation_mode"] == "seller") r = client.post(f"/api/chat/{gid}/personas/{sel['id']}/chat/start", headers=UH) assert r.status_code == 200, r.get_json() s2 = r.get_json()["session"] assert "task" in r.get_json() or "initiation_mode" in r.get_json() assert s2["messages"] == [] , "seller-initiated should not have a customer opener" print("[ok] seller-initiated session (no customer opener, seller must open)") # board r = client.get("/api/me/board", headers=UH) board = r.get_json()["board"] assert any(b["persona_id"] == pid and b["my_outcome"] == "won" for b in board) print("[ok] win/lose board reflects won persona") # weak-areas (no losses yet -> empty insight but endpoint works) r = client.get("/api/me/weak-areas", headers=UH) assert r.status_code == 200 print("[ok] weak-areas endpoint") # generate own persona (manual, mock) r = client.post("/api/me/personas/generate", json={"mode": "manual", "spec": {"target": "price-hardball"}}, headers=UH) assert r.status_code == 201, r.get_json() print("[ok] user generates own persona (manual)") # analytics (admin) r = client.get("/api/analytics", headers=AH) assert r.status_code == 200 a = r.get_json() assert a["overall"]["wins"] >= 1 print("[ok] admin analytics aggregates wins") print("\nALL E2E TESTS PASSED") if __name__ == "__main__": main()