- Add mock_chat_anthropic and mock_chat_deepseek fixtures to conftest.py - Update test_llm_factory to use monkeypatch and mock fixtures - Import ChatAnthropic and ChatDeepSeek in conftest.py for spec creation - Tests now pass without requiring ANTHROPIC_API_KEY or DEEPSEEK_API_KEY - Execution time reduced from failing after 0.12s to passing in 0.01s This change allows factory tests to run in isolation without external dependencies, making them consistent with the API test approach and suitable for any development environment or CI/CD pipeline.
262 lines
7.9 KiB
Python
262 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
|