Files
sales-trainer/backend/tests/test_persona_cardinality.py

108 lines
3.5 KiB
Python

"""Exact persona cardinality and schema contract tests."""
from __future__ import annotations
import copy
import pytest
from app.services.persona_generator import PersonaGenerator
class QueueLLM:
def __init__(self, results):
self.results = [copy.deepcopy(result) for result in results]
self.calls = 0
def complete_json(self, system_prompt, user_prompt, **kwargs):
self.calls += 1
return self.results[min(self.calls - 1, len(self.results) - 1)]
def _persona(index: int, tier: str, *, special: str = ""):
return {
"id": f"source-{index}",
"name": f"Persona {index}",
"tier": tier,
"channel": "line",
"initiation_mode": "customer" if index % 2 else "seller",
"profession": "owner",
"age_group": "adult",
"location": "Bangkok",
"product_context": "uses the product category",
"background": "background",
"income": "middle",
"lifestyle": "busy",
"personality": "careful",
"communication_style": "direct",
"budget": "1000",
"decision_timeline": "this month",
"goal": "solve pain",
"objections": ["price"],
"pains": [{"name": "pain", "fit": "strong", "description": "pain", "resolutionConditions": ["proof"]}],
"negotiation_levers": ["price"],
"opener": "Hello",
"special": special,
"difficulty": 3,
"notes": "notes",
"tolerance": 3,
"recontact": False,
}
def _valid_personas():
people = []
index = 1
for tier in ("A", "B", "C"):
for slot in range(5):
people.append(_persona(index, tier, special="wrong_text" if tier == "C" and slot == 0 else ""))
index += 1
return people
def _generate(results):
return PersonaGenerator(QueueLLM(results)).generate(
sales_kit={}, language="th", channel="line"
)
def test_generator_returns_exact_15_with_five_per_tier_and_special_c():
personas = _generate([{"personas": _valid_personas()}])
assert len(personas) == 15
assert {tier: sum(p["tier"] == tier for p in personas) for tier in "ABC"} == {
"A": 5, "B": 5, "C": 5
}
assert sum(p.get("special") == "wrong_text" for p in personas if p["tier"] == "C") >= 1
assert len({p["id"] for p in personas}) == 15
@pytest.mark.parametrize(
"mutate, message",
[
(lambda people: people.pop(), "exactly 15"),
(lambda people: people.__setitem__(0, {**people[0], "tier": "D"}), "tier"),
(lambda people: people.__setitem__(1, {**people[1], "id": people[0]["id"]}), "duplicate"),
(lambda people: people.__setitem__(0, {**people[0], "channel": "social"}), "channel"),
(lambda people: people.__setitem__(0, {**people[0], "initiation_mode": "f2f_call"}), "initiation"),
(lambda people: [p.update(special="") for p in people if p["tier"] == "C"], "wrong_text"),
],
)
def test_invalid_generation_retries_then_fails_closed(mutate, message):
people = _valid_personas()
mutate(people)
llm = QueueLLM([{"personas": people}])
generator = PersonaGenerator(llm)
with pytest.raises(ValueError, match=message):
generator.generate(sales_kit={}, language="th", channel="line")
assert llm.calls == 3
def test_malformed_provider_container_retries_then_fails():
llm = QueueLLM([{"personas": "not-a-list"}])
generator = PersonaGenerator(llm)
with pytest.raises(ValueError, match="persona"):
generator.generate(sales_kit={}, language="th", channel="line")
assert llm.calls == 3