- Fix production bug in send_whatsapp_message: access 'instance_url' instead of 'server_url' to match SendMessageScheme TypedDict definition - Update webhook test to use test_app with in-memory dependencies - Mock send_whatsapp_message to avoid real HTTP calls in tests - Add assertions to verify correct parameters passed to send_whatsapp_message Bug: send_whatsapp_message accessed request_data['server_url'] but SendMessageScheme defines field as 'instance_url', causing KeyError when webhook tries to send WhatsApp messages.
266 lines
7.9 KiB
Python
266 lines
7.9 KiB
Python
import pytest
|
|
from unittest.mock import Mock, MagicMock
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
|
|
from src.naliiabot.bot.agent.agent import Agent, AgentConfig
|
|
from src.naliiabot.bot.factories.llm_factory import LLMFactory
|
|
from httpx import ASGITransport, AsyncClient
|
|
from fastapi import FastAPI
|
|
from langgraph.checkpoint.memory import MemorySaver
|
|
from langchain_core.messages import AIMessage
|
|
from langchain_anthropic import ChatAnthropic
|
|
from langchain_deepseek import ChatDeepSeek
|
|
from naliiabot.bot.prompts.load_prompt import get_prompt_template
|
|
from naliiabot.bot.tools.naliia_tools import NaliiaTools
|
|
import naliiabotapi.api.dependencies as deps
|
|
from naliiabotapi.api.v1.endpoints.chat import router as chat_router
|
|
from naliiabotapi.api.v1.webhooks.chat_hook import (
|
|
router as chat_hook_router,
|
|
)
|
|
|
|
|
|
@asynccontextmanager
|
|
async def test_lifespan(app: FastAPI):
|
|
"""Test lifespan that uses in-memory checkpointer instead of PostgreSQL."""
|
|
# Use in-memory checkpointer for tests
|
|
checkpointer = MemorySaver()
|
|
|
|
# Use a mock model for tests to avoid needing API keys
|
|
# The mock needs to return proper AIMessage objects
|
|
mock_model = MagicMock()
|
|
|
|
def mock_invoke(*args, **kwargs):
|
|
"""Mock invoke that returns a proper AIMessage."""
|
|
return AIMessage(
|
|
content="This is a test response from the mock model."
|
|
)
|
|
|
|
mock_model.invoke = mock_invoke
|
|
mock_model.bind_tools = Mock(return_value=mock_model)
|
|
|
|
naliia_prompt = get_prompt_template("NALIIA_PROMPT")
|
|
naliia_tools = NaliiaTools().get_tools()
|
|
config = AgentConfig(system_prompt=naliia_prompt)
|
|
|
|
deps.agent_instance = Agent(
|
|
model=mock_model,
|
|
config=config,
|
|
checkpointer=checkpointer,
|
|
tools=naliia_tools,
|
|
)
|
|
|
|
yield
|
|
|
|
# Cleanup
|
|
deps.agent_instance = None
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_app():
|
|
"""Create a test FastAPI app with in-memory dependencies."""
|
|
app = FastAPI(
|
|
title="NaliiaBot API - Test",
|
|
description="Test API for NaliiaBot",
|
|
version="1.0.0",
|
|
root_path="/agent/api",
|
|
lifespan=test_lifespan,
|
|
)
|
|
|
|
app.include_router(chat_router)
|
|
app.include_router(chat_hook_router)
|
|
|
|
@app.get("/")
|
|
def read_root():
|
|
return {"Hello": "World"}
|
|
|
|
@app.get("/health")
|
|
def health_check():
|
|
return {"status": "ok"}
|
|
|
|
return app
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
async def async_client(test_app):
|
|
"""Fixture para un cliente HTTP asíncrono."""
|
|
|
|
async with AsyncClient(
|
|
transport=ASGITransport(app=test_app),
|
|
base_url="http://localhost:8010",
|
|
) as client:
|
|
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_tool():
|
|
"""Mock de herramienta."""
|
|
tool = Mock()
|
|
tool.name = "calculator"
|
|
tool.invoke = Mock(return_value="42")
|
|
return tool
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_model():
|
|
"""Mock de modelo LLM con interfaz completa."""
|
|
model = Mock()
|
|
model.bind_tools = Mock(return_value=model)
|
|
return model
|
|
|
|
|
|
@pytest.fixture
|
|
def default_config():
|
|
"""Configuración de test predecible."""
|
|
return AgentConfig(
|
|
system_prompt="Test prompt", max_iterations=5, timeout_seconds=10.0
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def agent(mock_model, default_config):
|
|
"""Agente básico sin herramientas."""
|
|
return Agent(model=mock_model, config=default_config)
|
|
|
|
|
|
@pytest.fixture
|
|
def agent_with_tool(mock_model, default_config, mock_tool):
|
|
"""Agente con una herramienta mock."""
|
|
return Agent(
|
|
model=mock_model, config=default_config, tools=[mock_tool]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def anthropic_model():
|
|
llmFactory = LLMFactory("anthropic")
|
|
anthropic_model = llmFactory.get_model()
|
|
|
|
return anthropic_model
|
|
|
|
|
|
@pytest.fixture
|
|
def send_message_payload():
|
|
"""Payload de ejemplo para enviar mensajes al webhook."""
|
|
return {
|
|
"event": "messages.upsert",
|
|
"instance": "ALEJANDRO AYALA",
|
|
"data": {
|
|
"key": {
|
|
"remoteJid": "5512992544490@s.whatsapp.net",
|
|
"remoteJidAlt": "5512992544490@s.whatsapp.net",
|
|
"fromMe": False,
|
|
"id": "AC4367597BDC50537133B9C8848B2E62",
|
|
"participant": "",
|
|
"addressingMode": "lid",
|
|
},
|
|
"pushName": "Alejandro",
|
|
"status": "DELIVERY_ACK",
|
|
"message": {
|
|
"conversation": "Hola",
|
|
"messageContextInfo": {
|
|
"threadId": [],
|
|
"deviceListMetadata": {
|
|
"senderKeyIndexes": [],
|
|
"recipientKeyIndexes": [],
|
|
"senderKeyHash": {
|
|
"0": 154,
|
|
"1": 106,
|
|
"2": 69,
|
|
"3": 189,
|
|
"4": 90,
|
|
"5": 105,
|
|
"6": 172,
|
|
"7": 243,
|
|
"8": 27,
|
|
"9": 98,
|
|
},
|
|
"senderTimestamp": {
|
|
"low": 1772156772,
|
|
"high": 0,
|
|
"unsigned": "true",
|
|
},
|
|
"recipientKeyHash": {
|
|
"0": 16,
|
|
"1": 98,
|
|
"2": 75,
|
|
"3": 100,
|
|
"4": 131,
|
|
"5": 43,
|
|
"6": 24,
|
|
"7": 225,
|
|
"8": 244,
|
|
"9": 126,
|
|
},
|
|
"recipientTimestamp": {
|
|
"low": 1772155597,
|
|
"high": 0,
|
|
"unsigned": True,
|
|
},
|
|
},
|
|
"deviceListMetadataVersion": 2,
|
|
"messageSecret": {
|
|
"0": 100,
|
|
"1": 47,
|
|
"2": 112,
|
|
"3": 133,
|
|
"4": 255,
|
|
"5": 156,
|
|
"6": 129,
|
|
"7": 57,
|
|
"8": 239,
|
|
"9": 219,
|
|
"10": 74,
|
|
"11": 171,
|
|
"12": 212,
|
|
"13": 150,
|
|
"14": 112,
|
|
"15": 83,
|
|
"16": 123,
|
|
"17": 78,
|
|
"18": 189,
|
|
"19": 13,
|
|
"20": 51,
|
|
"21": 133,
|
|
"22": 129,
|
|
"23": 6,
|
|
"24": 197,
|
|
"25": 119,
|
|
"26": 197,
|
|
"27": 56,
|
|
"28": 87,
|
|
"29": 176,
|
|
"30": 135,
|
|
"31": 62,
|
|
},
|
|
},
|
|
},
|
|
"messageType": "conversation",
|
|
"messageTimestamp": 1772168493,
|
|
"instanceId": "75a877f1-d772-422b-a6f1-6ac470af5663",
|
|
"source": "android",
|
|
},
|
|
"destination": "http://192.168.58.110:8010/webhook",
|
|
"date_time": "2026-02-27T02:01:34.178Z",
|
|
"sender": "573016859278@s.whatsapp.net",
|
|
"server_url": "http://localhost:8080",
|
|
"apikey": "98B6B820AFC3-4544-9A4C-011B0719C2D1",
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_chat_anthropic():
|
|
"""Mock para ChatAnthropic que no requiere API key."""
|
|
mock = Mock(spec=ChatAnthropic)
|
|
mock.model_name = "claude-sonnet-4-5-20250929"
|
|
return mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_chat_deepseek():
|
|
"""Mock para ChatDeepSeek que no requiere API key."""
|
|
mock = Mock(spec=ChatDeepSeek)
|
|
mock.model_name = "deepseek-chat"
|
|
return mock
|