- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant, 30-day UTC trial on first login, revocable, one-time credential delivery via optional SES/webhook (never persisted). Adds boto3 dependency. - Analytics/report/export/privacy: shared bounded scan budget across users/groups/ sessions, tenant-consistent session/user/group joins, scalar-only CSV export (no nested persisted-value stringification). - Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat; client sees is_owned only, never owner_user_id. - Lifecycle/races: status transition validation, analyzing is an in-progress gate (no duplicate reanalysis), structured-ready publication, stale-variant revalidation. - Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime, provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona trait validation. - Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery, role-aware completed-chat dashboard routing. - Frontend: Training→product→personas→practice flow, demo/role/demo guards, is_owned-based ownership display, 320×568 and 500×768 responsive E2E. - 8 independent exact-five-key review scopes passed; backend 509, frontend 26, production build 1775 modules, isolated E2E 15.
134 lines
4.8 KiB
Python
134 lines
4.8 KiB
Python
"""All roles train for real: no preview mode, one-shot trainee sessions.
|
|
|
|
Every chat/session is a real trainee attempt (mode='trainee') regardless of
|
|
role. Requesting 'preview' is ignored and becomes a normal trainee session.
|
|
A persona may be chatted once per user; the same persona can be trained by
|
|
many users. Admin/super_admin sessions count toward their own board and the
|
|
org analytics just like trainee sessions.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from scripts.mock_llm import MockLLM
|
|
|
|
|
|
def _headers(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
def _setup(user_store, login):
|
|
user_store.complete_setup(
|
|
"admin",
|
|
"admin@example.com",
|
|
"admin-ready-password",
|
|
accepted_terms=True,
|
|
accepted_terms_at="2026-08-13T00:00:00Z",
|
|
)
|
|
return login("admin", "admin-ready-password")["token"]
|
|
|
|
|
|
def _ready_group(client, token):
|
|
client.application.extensions["llm"] = MockLLM()
|
|
response = client.post(
|
|
"/api/groups",
|
|
json={"product": "CRM", "segment": "SME"},
|
|
headers=_headers(token),
|
|
)
|
|
assert response.status_code == 201, response.get_json()
|
|
gid = response.get_json()["group"]["id"]
|
|
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=_headers(token))
|
|
assert analyzed.status_code == 200, analyzed.get_json()
|
|
pid = analyzed.get_json()["personas"][0]["id"]
|
|
return gid, pid
|
|
|
|
|
|
def test_admin_session_is_real_trainee_and_counts(client, user_store, login):
|
|
super_token = _setup(user_store, login)
|
|
gid, pid = _ready_group(client, super_token)
|
|
admin = user_store.create_user(
|
|
org_id="org-default",
|
|
username="realtrainer-admin",
|
|
password="realtrainer-admin-password",
|
|
name="Real Trainer Admin",
|
|
role="admin",
|
|
must_setup=False,
|
|
)
|
|
admin_token = login(admin["username"], "realtrainer-admin-password")["token"]
|
|
headers = _headers(admin_token)
|
|
|
|
started = client.post(
|
|
f"/api/chat/{gid}/personas/{pid}/chat/start",
|
|
json={"scenario": "social", "locale": "th"},
|
|
headers=headers,
|
|
)
|
|
assert started.status_code == 200, started.get_json()
|
|
session = started.get_json()["session"]
|
|
# Admins train for real: mode is trainee (one-shot), not a repeatable preview.
|
|
assert session["mode"] == "trainee"
|
|
assert session["status"] == "active"
|
|
|
|
sent = client.post(
|
|
f"/api/chat/{gid}/personas/{pid}/chat/send",
|
|
json={"text": "ขอทราบราคาและวิธีเริ่มใช้งาน"},
|
|
headers=headers,
|
|
)
|
|
assert sent.status_code == 200, sent.get_json()
|
|
assert sent.get_json()["session"]["mode"] == "trainee"
|
|
|
|
# The admin's real session is now a normal trainee session: it shows on
|
|
# their board (no longer excluded as preview was), so the persona is
|
|
# correctly marked as trained and locked for this admin.
|
|
board = client.get("/api/me/board", headers=headers)
|
|
assert board.status_code == 200
|
|
item = next((b for b in board.get_json()["board"]
|
|
if b.get("persona_id") == pid and b.get("group_id") == gid), None)
|
|
assert item is not None, "admin's trained persona must appear on the board"
|
|
assert item["my_outcome"] in ("won", "lost")
|
|
|
|
|
|
def test_preview_mode_request_becomes_trainee_session(client, user_store, login):
|
|
super_token = _setup(user_store, login)
|
|
gid, pid = _ready_group(client, super_token)
|
|
user = user_store.create_user(
|
|
org_id="org-default",
|
|
username="plain-user",
|
|
password="plain-user-password",
|
|
name="Plain User",
|
|
role="user",
|
|
must_setup=False,
|
|
)
|
|
token = login(user["username"], "plain-user-password")["token"]
|
|
|
|
# Requesting 'preview' no longer exists; it is ignored and becomes a real
|
|
# one-shot trainee session for every role.
|
|
response = client.post(
|
|
f"/api/chat/{gid}/personas/{pid}/chat/start",
|
|
json={"scenario": "social", "mode": "preview"},
|
|
headers=_headers(token),
|
|
)
|
|
|
|
assert response.status_code == 200, response.get_json()
|
|
assert response.get_json()["session"]["mode"] == "trainee"
|
|
|
|
|
|
def test_admin_cannot_start_session_in_another_org(client, user_store, login):
|
|
super_token = _setup(user_store, login)
|
|
gid, pid = _ready_group(client, super_token)
|
|
other_org = user_store.create_org("Other Org")
|
|
other_admin = user_store.create_user(
|
|
org_id=other_org["id"],
|
|
username="other-org-admin",
|
|
password="other-org-password",
|
|
name="Other Org Admin",
|
|
role="admin",
|
|
must_setup=False,
|
|
)
|
|
token = login(other_admin["username"], "other-org-password")["token"]
|
|
|
|
response = client.post(
|
|
f"/api/chat/{gid}/personas/{pid}/chat/start",
|
|
json={"scenario": "social"},
|
|
headers=_headers(token),
|
|
)
|
|
|
|
assert response.status_code == 404
|