144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
"""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"
|