"""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"