Files
HermesMessages/backend/tests/test_bot_engine.py
Hanzo_dev 798bd14312 feat: initial commit — HermesMessages SaaS platform
Backend (FastAPI + Python 3.12):
- Multi-tenant auth with JWT: login, register, refresh, Meta OAuth
- Business & BusinessConfig management
- WhatsApp webhook with HMAC signature verification
- Bot engine powered by Claude AI
- Calendar availability with Redis caching
- Reservations CRUD with status management
- Dashboard analytics (stats, agenda, peak hours)
- Billing & plan management
- Admin panel with platform-wide stats
- Async bcrypt via asyncio.to_thread
- IntegrityError handling for concurrent registration race conditions

Frontend (React 18 + Vite + Tailwind CSS):
- Multi-step guided registration form with helper text on every field
- Login page with show/hide password toggle
- Protected routes with AuthContext
- Dashboard with stats cards, bar chart, and daily agenda
- Reservations list with search, filters, and inline status actions
- Calendar with weekly view, slot availability, and date blocking
- Config page: business info, schedules, bot personality
- Billing page with plan comparison and usage bar

Design system:
- Bricolage Grotesque + DM Sans typography
- Emerald primary palette with semantic color tokens
- scale(0.97) button press feedback, ease-out animations
- Skeleton loaders, stagger animations, prefers-reduced-motion support
- Accessible: aria-labels, visible focus rings, 4.5:1 contrast

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:49:41 -05:00

141 lines
5.1 KiB
Python

from datetime import date, time
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from app.modules.bot_engine.prompt import build_system_prompt
from app.modules.calendar.service import _generate_slots
from app.modules.bot_engine.schemas import CollectedData, ConversationContext
from app.modules.calendar.schemas import DayAvailability, SlotRead
def _make_config(**kwargs):
config = MagicMock()
config.tone = kwargs.get("tone", "formal")
config.assistant_name = kwargs.get("assistant_name", "Hermes")
config.open_days = kwargs.get("open_days", [0, 1, 2, 3, 4])
config.open_time = kwargs.get("open_time", time(9, 0))
config.close_time = kwargs.get("close_time", time(18, 0))
config.slot_duration = kwargs.get("slot_duration", 60)
config.max_per_slot = kwargs.get("max_per_slot", 2)
return config
def _make_business(**kwargs):
b = MagicMock()
b.name = kwargs.get("name", "Restaurante Demo")
b.id = 1
b.whatsapp_phone_number_id = "phone-id"
b.whatsapp_access_token = "token"
return b
def test_build_system_prompt_contains_business_name():
business = _make_business(name="Clínica Sol")
config = _make_config(assistant_name="Sol")
context = ConversationContext(phone="5491100000000", business_id=1)
prompt = build_system_prompt(business, config, None, context)
assert "Clínica Sol" in prompt
assert "Sol" in prompt
def test_build_system_prompt_formal_tone():
business = _make_business()
config = _make_config(tone="formal")
context = ConversationContext(phone="549", business_id=1)
prompt = build_system_prompt(business, config, None, context)
assert "formal" in prompt.lower()
def test_build_system_prompt_casual_tone():
business = _make_business()
config = _make_config(tone="casual")
context = ConversationContext(phone="549", business_id=1)
prompt = build_system_prompt(business, config, None, context)
assert "amigable" in prompt.lower()
def test_build_system_prompt_with_slots():
business = _make_business()
config = _make_config()
context = ConversationContext(phone="549", business_id=1)
availability = DayAvailability(
date=date(2026, 5, 5),
is_open=True,
slots=[
SlotRead(time_start=time(10, 0), time_end=time(11, 0), available=2, max_per_slot=2),
SlotRead(time_start=time(11, 0), time_end=time(12, 0), available=1, max_per_slot=2),
],
)
prompt = build_system_prompt(business, config, availability, context)
assert "10:00" in prompt
assert "11:00" in prompt
def test_build_system_prompt_no_availability():
business = _make_business()
config = _make_config()
context = ConversationContext(phone="549", business_id=1)
availability = DayAvailability(date=date(2026, 5, 5), is_open=False, slots=[])
prompt = build_system_prompt(business, config, availability, context)
assert "No hay disponibilidad" in prompt
def test_build_system_prompt_collected_data_shown():
business = _make_business()
config = _make_config()
context = ConversationContext(
phone="549",
business_id=1,
collected_data=CollectedData(
client_name="Juan",
date="2026-05-05",
time_start="10:00",
party_size=3,
),
)
prompt = build_system_prompt(business, config, None, context)
assert "Juan" in prompt
assert "2026-05-05" in prompt
assert "10:00" in prompt
assert "3" in prompt
def test_build_system_prompt_requires_json_response():
business = _make_business()
config = _make_config()
context = ConversationContext(phone="549", business_id=1)
prompt = build_system_prompt(business, config, None, context)
assert "create_reservation" in prompt
assert "collect_more" in prompt
assert "cancel" in prompt
@pytest.mark.skip(reason="requiere asyncpg — test de integración")
async def test_process_message_calls_claude_and_sends_reply(db, redis_mock):
from app.modules.bot_engine import service as bot_service
from app.modules.bot_engine.schemas import BotResponse, CollectedData
business = _make_business()
bot_response = BotResponse(
message="Hola, ¿cuál es tu nombre?",
action="collect_more",
collected_data=CollectedData(),
)
with (
patch.object(bot_service, "_load_context", return_value=ConversationContext(phone="549", business_id=1)),
patch("app.modules.bot_engine.service.get_business_config", return_value=_make_config()),
patch.object(bot_service, "_call_claude", return_value=bot_response),
patch.object(bot_service, "_save_context"),
patch("app.modules.bot_engine.service.send_text_message", new_callable=AsyncMock) as mock_send,
patch("app.modules.bot_engine.service.aioredis.from_url", return_value=redis_mock),
):
await bot_service.process_message(db=db, phone="549", text="Hola", business=business)
mock_send.assert_called_once_with(
phone_number_id="phone-id",
access_token="token",
to="549",
text="Hola, ¿cuál es tu nombre?",
)