Files
sales-trainer/backend/tests/test_persona_initiation.py
Macky 3c22d88bcd feat: demo SaaS + training flow security hardening (8/8 review gate passed)
- Demo accounts: super_admin-only provisioning into isolated DEMO_ORG_ID tenant,
  30-day UTC trial on first login, revocable, one-time credential delivery via
  optional SES/webhook (never persisted). Adds boto3 dependency.
- Analytics/report/export/privacy: shared bounded scan budget across users/groups/
  sessions, tenant-consistent session/user/group joins, scalar-only CSV export
  (no nested persisted-value stringification).
- Ownership/tenant isolation: canonical owner-tenant predicate for list/read/chat;
  client sees is_owned only, never owner_user_id.
- Lifecycle/races: status transition validation, analyzing is an in-progress gate
  (no duplicate reanalysis), structured-ready publication, stale-variant revalidation.
- Auth/setup/consent/JWT/OAuth/config: fail-closed consent, bounded JWT lifetime,
  provider-subject atomic OAuth identity, repeated-secret rejection, strict Persona
  trait validation.
- Chat/session/privacy: pre-seller opener redaction, corrupt-session recovery,
  role-aware completed-chat dashboard routing.
- Frontend: Training→product→personas→practice flow, demo/role/demo guards,
  is_owned-based ownership display, 320×568 and 500×768 responsive E2E.
- 8 independent exact-five-key review scopes passed; backend 509, frontend 26,
  production build 1775 modules, isolated E2E 15.
2026-08-25 06:39:06 +07:00

82 lines
2.6 KiB
Python

"""Persona-specific channel and initiation contracts."""
from __future__ import annotations
import json
import pytest
from app.services.persona_generator import PersonaGenerator
from app.services.store import ensure_persona_shape, validate_persona_traits
from app.api.chat_routes import _scenario_config
class StubPersonaLLM:
def __init__(self, personas):
self.personas = personas
def complete_json(self, system_prompt, user_prompt, **kwargs):
return {"personas": self.personas}
def _persona(channel="line", initiation_mode="customer", **extra):
value = {
"name": "Customer",
"tier": "B",
"channel": channel,
"initiation_mode": initiation_mode,
}
value.update(extra)
return value
def _fifteen(**kwargs):
return [_persona(**kwargs) for _ in range(15)]
def test_persona_owns_initiation_even_when_scenario_changes():
customer = ensure_persona_shape(_persona(initiation_mode="customer"))
seller = ensure_persona_shape(_persona(initiation_mode="seller"))
_, customer_mode = _scenario_config("f2f_call", customer, "th")
_, seller_mode = _scenario_config("social", seller, "th")
assert customer_mode == "customer"
assert seller_mode == "seller"
def test_channel_and_initiation_are_retained_in_normalized_persona():
persona = ensure_persona_shape(_persona(channel="facebook", initiation_mode="seller"))
assert persona["channel"] == "facebook"
assert persona["initiation_mode"] == "seller"
validate_persona_traits(persona)
def test_shape_preserves_explicit_invalid_initiation_for_rejection():
persona = ensure_persona_shape(_persona(initiation_mode="f2f_call"))
assert persona["initiation_mode"] == "f2f_call"
with pytest.raises(ValueError, match="initiation_mode"):
validate_persona_traits(persona)
def test_invalid_channel_is_rejected_not_mapped_to_social():
generator = PersonaGenerator(StubPersonaLLM(_fifteen(channel="social")))
with pytest.raises(ValueError, match="channel"):
generator.generate(sales_kit={}, language="th", channel="facebook")
def test_invalid_initiation_is_rejected_not_mapped_to_customer():
generator = PersonaGenerator(StubPersonaLLM(_fifteen(initiation_mode="f2f_call")))
with pytest.raises(ValueError, match="initiation_mode"):
generator.generate(sales_kit={}, language="th", channel="facebook")
def test_generator_rejects_invalid_requested_channel():
generator = PersonaGenerator(StubPersonaLLM(_fifteen()))
with pytest.raises(ValueError, match="channel"):
generator.generate(sales_kit={}, language="th", channel="social")