feat(personas): 'create persona from this persona' (variant) — practice the same challenge, new identity

Each persona is one-shot (chat once = win/lose locked). To keep training repeatable:
- New endpoint POST /api/groups/<gid>/personas/<pid>/variant creates a NEW persona that is
  a fresh incarnation of the source: LOCKS pain points, objections, negotiation levers,
  tolerance, special/recontact, goal, budget, difficulty, tier, product_context — but VARYS
  name/profession/age/location/background/personality/opener so it isn't an identical copy.
- Added to the same group as a distinct persona (fresh not_tried, so chat-able again).
- UI: on the Personas page, a finished (won/lost) persona gets a
  'สร้างบุคคลต้นแบบจากต้นแบบนี้' button; reload shows the variant.
All 10 backend suites pass. Rebuilt dist.
This commit is contained in:
Macky
2026-08-09 12:43:00 +07:00
parent 8771438aef
commit 10c7d01236
29 changed files with 217 additions and 30 deletions

View File

@@ -0,0 +1,54 @@
"""Test: create a persona VARIANT from an existing persona (fresh identity, locked core traits)."""
import 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}"}
# admin creates group + analyze (15 personas)
r = C.post("/api/groups", headers=AH, json={"product":"POS CRM","segment":"SME restaurants","language":"th"})
gid = r.get_json()["group"]["id"]
C.post(f"/api/groups/{gid}/analyze", headers=AH)
ps = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]
src = ps[0]
print("source:", src["name"], "id:", src["id"], "| pains:", len(src.get("pains", [])) if isinstance(src.get("pains"), list) else "")
# create a variant
r = C.post(f"/api/groups/{gid}/personas/{src['id']}/variant", headers=AH)
assert r.status_code == 201, (r.status_code, r.get_json())
var = r.get_json()["persona"]
print("[ok] variant created:", var.get("name"), "| id:", var.get("id"))
# it's a NEW id (not the source)
assert var["id"] != src["id"], "variant must have a new id"
# core traits locked (pains present as objects w/ description)
assert isinstance(var.get("pains", []), list), "variant must keep pains"
# added to the group
ps2 = C.get(f"/api/groups/{gid}/personas", headers=AH).get_json()["personas"]
ids = [p["id"] for p in ps2]
assert var["id"] in ids, "variant must be in the group personas"
print("[ok] variant added to group (now", len(ps2), "personas)")
# the variant can be chatted fresh (not_tried for a fresh trainee)
UT = tok("admin", "newpass"); UH = {"Authorization": f"Bearer {UT}"}
board = C.get("/api/me/board", headers=UH).get_json()
vp = next((x for x in board.get("board", []) if x["persona_id"] == var["id"]), None)
print("[ok] variant appears on my board:", (vp or {}).get("my_outcome") if vp else None)
print("ALL VARIANT TESTS PASSED")