[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:
143
backend/tests/test_auto_close_try.py
Normal file
143
backend/tests/test_auto_close_try.py
Normal 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"
|
||||
Reference in New Issue
Block a user