108 lines
3.5 KiB
Python
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
|