[verified] fix live-QA UX: no JSON leak in chat, greeting openers (wrong_text cools off post-first-reply), auto-close+summarize on buy/walk/try, remove Finish button

This commit is contained in:
Macky
2026-08-18 09:42:05 +07:00
parent 2c40fc7502
commit ffb7cdda31
11 changed files with 379 additions and 46 deletions

View File

@@ -0,0 +1,143 @@
"""Auto-close contract: when the customer reaches a decision (buy/walk/try),
the chat route MUST close + summarize automatically — no manual finish needed.
Covers the 'trial first, come back later' (try) path specifically.
"""
from __future__ import annotations
from app.llm import LLMError
class StubSim:
"""Stubbed simulator: a persona reply + a per-turn judge that returns 'try'."""
def __init__(self, decision: str = "try"):
self.decision = decision
self.judge_verdict = {
"outcome": "lost",
"score": 55,
"pain": "wants to trial before committing",
"why": "customer trialing first, not closed yet",
"failurePoints": [],
"coaching": [],
"painProgress": {"trial": 60},
}
def persona_reply(self, **kwargs):
return "ถ้าอย่างนั้น ขอทดลองใช้ก่อนได้ไหมคะ", {"decision": "none", "mood": 1}
def evaluate_turn(self, **kwargs):
return {
"mood": 1,
"decision": self.decision,
"score_delta": 3,
"reason": "ลูกค้าตัดสินใจลองใช้ก่อน",
}
def judge(self, **kwargs):
return dict(self.judge_verdict)
def _auth_headers(token):
return {"Authorization": f"Bearer {token}"}
def _ready(client, user_store, login):
from scripts.mock_llm import MockLLM
user_store.complete_setup(
"admin", "admin@example.com", "admin-ready-password",
accepted_terms=True, accepted_terms_at="2026-08-13T00:00:00Z",
)
token = login("admin", "admin-ready-password")["token"]
headers = _auth_headers(token)
client.application.extensions["llm"] = MockLLM()
response = client.post(
"/api/groups", json={"product": "CRM", "segment": "SME"}, headers=headers
)
assert response.status_code == 201, response.get_json()
gid = response.get_json()["group"]["id"]
analyzed = client.post(f"/api/groups/{gid}/analyze", headers=headers)
assert analyzed.status_code == 200, analyzed.get_json()
pid = analyzed.get_json()["personas"][0]["id"]
return gid, pid, headers
def test_try_decision_auto_closes_with_trial_system_note(client, user_store, login, monkeypatch):
gid, pid, headers = _ready(client, user_store, login)
started = client.post(
f"/api/chat/{gid}/personas/{pid}/chat/start",
json={"scenario": "social", "locale": "th", "mode": "preview"},
headers=headers,
)
assert started.status_code == 200, started.get_json()
assert started.get_json()["session"]["status"] == "active"
stub = StubSim(decision="try")
monkeypatch.setattr("app.api.chat_routes._sim", lambda g, p: stub)
sent = client.post(
f"/api/chat/{gid}/personas/{pid}/chat/send",
json={"text": "ได้เลยครับ ลองใช้ก่อนได้"},
headers=headers,
)
assert sent.status_code == 200, sent.get_json()
body = sent.get_json()
# Auto-close: the session must be finished with a summary, no manual step.
assert body["finished"] is True
assert body["session"]["status"] == "finished"
assert body["debrief"] is not None
assert body["debrief"]["score"] == 55
# A system note about the trial + coming back must be in the transcript.
texts = [m.get("text", "") for m in body["messages"] if m.get("role") == "system"]
assert any("ลองใช้" in t or "trial" in t.lower() for t in texts)
def test_buy_decision_auto_closes_won(client, user_store, login, monkeypatch):
gid, pid, headers = _ready(client, user_store, login)
client.post(
f"/api/chat/{gid}/personas/{pid}/chat/start",
json={"scenario": "social", "locale": "th", "mode": "preview"},
headers=headers,
)
stub = StubSim(decision="buy")
stub.judge_verdict["outcome"] = "won"
monkeypatch.setattr("app.api.chat_routes._sim", lambda g, p: stub)
sent = client.post(
f"/api/chat/{gid}/personas/{pid}/chat/send",
json={"text": "โอเค เอาเลยครับ"},
headers=headers,
)
assert sent.status_code == 200, sent.get_json()
body = sent.get_json()
assert body["finished"] is True
assert body["outcome"] == "won"
assert body["session"]["status"] == "finished"
def test_walk_decision_auto_closes_lost(client, user_store, login, monkeypatch):
gid, pid, headers = _ready(client, user_store, login)
client.post(
f"/api/chat/{gid}/personas/{pid}/chat/start",
json={"scenario": "social", "locale": "th", "mode": "preview"},
headers=headers,
)
stub = StubSim(decision="walk")
stub.judge_verdict["outcome"] = "lost"
monkeypatch.setattr("app.api.chat_routes._sim", lambda g, p: stub)
sent = client.post(
f"/api/chat/{gid}/personas/{pid}/chat/send",
json={"text": "งั้นไม่เอาดีกว่าครับ"},
headers=headers,
)
assert sent.status_code == 200, sent.get_json()
body = sent.get_json()
assert body["finished"] is True
assert body["outcome"] == "lost"
assert body["session"]["status"] == "finished"