- User id/login = username (was email). Email is a separate settable field. - Default admin: username admin / password 1234, must_setup=True. - Login forces /setup on first login: set email + change password, then clears must_setup. - New /api/auth/setup endpoint; JWT sub = username; admin routes use username. - Frontend: Login uses username, router guard forces /setup, new Setup.vue (email + new password + confirm), i18n EN/TH. - Tests: test_setup.py added; all suites adapted (m0/m1/routes/security/setup/e2e) PASS.
142 lines
5.8 KiB
Python
142 lines
5.8 KiB
Python
"""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={"username": "admin", "password": "1234"})
|
|
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", "username": "trainee9", "password": "pass123", "role": "user"}, headers=AH)
|
|
r = client.post("/api/auth/login", json={"username": "trainee9", "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()
|