From 1b5303b44f9b5861535335ce545e205093d06b23 Mon Sep 17 00:00:00 2001 From: aserrador Date: Tue, 24 Feb 2026 23:11:04 -0500 Subject: [PATCH] fix: unnecesary tests --- tests/test_naliia_agent_tools.py | 475 ------------------------------- tests/test_tool_registry.py | 434 ---------------------------- 2 files changed, 909 deletions(-) delete mode 100644 tests/test_naliia_agent_tools.py delete mode 100644 tests/test_tool_registry.py diff --git a/tests/test_naliia_agent_tools.py b/tests/test_naliia_agent_tools.py deleted file mode 100644 index 478cca4..0000000 --- a/tests/test_naliia_agent_tools.py +++ /dev/null @@ -1,475 +0,0 @@ -import pytest -from unittest.mock import Mock, MagicMock -from langchain_core.messages import HumanMessage, AIMessage, ToolMessage -from src.naliiabot.bot.agent.agent import Agent, AgentConfig - - -""" -Tests robustos para las tools/herramientas de la clase Agent. - -Estrategia: -- Tests de ejecución de herramientas individuales -- Tests de manejo de errores -- Tests del nodo de tools -- Tests de integración con el flujo del agente -""" - - -# ============================================================================ -# TESTS DE EJECUCION DE HERRAMIENTAS INDIVIDUALES -# ============================================================================ - - -class TestExecuteTool: - """Pruebas para la ejecución de herramientas individuales.""" - - def test_execute_tool_success(self, agent_with_tool): - """ - DADO: Una herramienta registrada en el agente - CUANDO: Se ejecuta exitosamente - ENTONCES: Retorna ToolMessage con status 'success' - """ - tool_call = { - "name": "calculator", - "id": "tool_123", - "args": {"x": 5, "y": 7} - } - - result = agent_with_tool._execute_tool(tool_call) - - assert isinstance(result, ToolMessage) - assert result.status == "success" - assert result.tool_call_id == "tool_123" - assert result.content == "42" - - def test_execute_tool_not_found(self, agent_with_tool): - """ - DADO: Una herramienta que no existe - CUANDO: Se intenta ejecutar - ENTONCES: Retorna ToolMessage con error - """ - tool_call = { - "name": "nonexistent_tool", - "id": "tool_456", - "args": {} - } - - result = agent_with_tool._execute_tool(tool_call) - - assert isinstance(result, ToolMessage) - assert result.status == "error" - assert "not found" in result.content.lower() - - def test_execute_tool_with_exception(self, mock_model, default_config): - """ - DADO: Una herramienta que lanza excepción - CUANDO: Se ejecuta - ENTONCES: Captura el error y retorna ToolMessage con estado error - """ - failing_tool = Mock() - failing_tool.name = "failing_tool" - failing_tool.invoke = Mock(side_effect=ValueError("Tool error")) - - agent = Agent( - model=mock_model, - config=default_config, - tools=[failing_tool] - ) - - tool_call = { - "name": "failing_tool", - "id": "tool_789", - "args": {"param": "value"} - } - - result = agent._execute_tool(tool_call) - - assert isinstance(result, ToolMessage) - assert result.status == "error" - assert "Tool error" in result.content - - def test_execute_tool_with_no_args(self, agent_with_tool): - """ - DADO: Una herramienta sin argumentos en tool_call - CUANDO: Se ejecuta - ENTONCES: Usa dict vacío como argumentos - """ - tool_call = { - "name": "calculator", - "id": "tool_abc" - # Sin 'args' - } - - result = agent_with_tool._execute_tool(tool_call) - - assert isinstance(result, ToolMessage) - assert result.tool_call_id == "tool_abc" - - -# ============================================================================ -# TESTS DEL NODO DE HERRAMIENTAS (TOOL NODE) -# ============================================================================ - - -class TestToolNode: - """Pruebas para el nodo que ejecuta herramientas.""" - - def test_tool_node_with_tool_calls(self, agent_with_tool): - """ - DADO: Un estado con mensaje AIMessage que contiene tool_calls - CUANDO: Se ejecuta el tool_node - ENTONCES: Retorna estado actualizado con ToolMessages - """ - # Mock del modelo para generar tool_call - ai_message = AIMessage( - content="Using calculator", - tool_calls=[ - { - "name": "calculator", - "id": "call_1", - "args": {"x": 10} - } - ] - ) - - state = { - "messages": [ - HumanMessage(content="Calculate"), - ai_message - ] - } - - result = agent_with_tool._tool_node(state) - - assert "messages" in result - assert len(result["messages"]) == 3 # original 2 + 1 ToolMessage - assert isinstance(result["messages"][-1], ToolMessage) - - def test_tool_node_without_tool_calls(self, agent): - """ - DADO: Un estado con mensaje sin tool_calls - CUANDO: Se ejecuta el tool_node - ENTONCES: Retorna estado sin cambios - """ - state = { - "messages": [ - HumanMessage(content="Hello"), - AIMessage(content="Hi there!") - ] - } - - result = agent._tool_node(state) - - assert result["messages"] == state["messages"] - - def test_tool_node_multiple_tool_calls(self, mock_model, default_config): - """ - DADO: Un AIMessage con múltiples tool_calls - CUANDO: Se ejecuta el tool_node - ENTONCES: Ejecuta todas las herramientas y retorna múltiples ToolMessages - """ - tool1 = Mock() - tool1.name = "tool_a" - tool1.invoke = Mock(return_value="Result A") - - tool2 = Mock() - tool2.name = "tool_b" - tool2.invoke = Mock(return_value="Result B") - - agent = Agent( - model=mock_model, - config=default_config, - tools=[tool1, tool2] - ) - - ai_message = AIMessage( - content="Using tools", - tool_calls=[ - {"name": "tool_a", "id": "call_1", "args": {}}, - {"name": "tool_b", "id": "call_2", "args": {}} - ] - ) - - state = { - "messages": [ai_message] - } - - result = agent._tool_node(state) - - # 1 AIMessage + 2 ToolMessages - assert len(result["messages"]) == 3 - assert all(isinstance(msg, (AIMessage, ToolMessage)) for msg in result["messages"]) - - def test_tool_node_post_hook_called(self, agent_with_tool): - """ - DADO: Un agente con post_tool_hook registrado - CUANDO: Se ejecuta una herramienta - ENTONCES: Se invoca el hook después de cada ejecución - """ - hook = Mock() - agent_with_tool.set_post_tool_hook(hook) - - ai_message = AIMessage( - content="Test", - tool_calls=[ - { - "name": "calculator", - "id": "call_1", - "args": {} - } - ] - ) - - state = { - "messages": [ai_message] - } - - agent_with_tool._tool_node(state) - - # Hook debe ser llamado una vez por herramienta - assert hook.call_count == 1 - - -# ============================================================================ -# TESTS DE INTEGRACIÓN CON FLUJO COMPLETO -# ============================================================================ - - -class TestToolsIntegration: - """Pruebas de integración de tools en el flujo completo del agente.""" - - def test_agent_invokes_tool_in_flow(self, agent_with_tool): - """ - DADO: Un agente con herramientas configurado - CUANDO: Se invocan herramientas en la conversación - ENTONCES: El estado se actualiza con los resultados - """ - # Mock del modelo para retornar un AIMessage con tool_calls - ai_response = AIMessage( - content="I'll use the calculator", - tool_calls=[ - { - "name": "calculator", - "id": "call_x", - "args": {"result": "42"} - } - ] - ) - agent_with_tool._model.bind_tools().invoke.return_value = ai_response - - state = { - "messages": [HumanMessage(content="Calculate 2+2")] - } - - result = agent_with_tool.invoke(state) - - assert "messages" in result - # Debe contener original + AIMessage + ToolMessage - assert len(result["messages"]) >= 2 - - def test_tools_are_bound_to_model(self, agent_with_tool): - """ - DADO: Un agente con herramientas - CUANDO: Se invoca el LLM - ENTONCES: Las herramientas están vinculadas al modelo - """ - initial_state = { - "messages": [HumanMessage(content="Hi")] - } - - try: - agent_with_tool.invoke(initial_state) - except: - # Es ok si falla por mocks incompletos - pass - - # Verificar que bind_tools fue llamado - agent_with_tool._model.bind_tools.assert_called() - - def test_tools_property_returns_copy(self, agent_with_tool): - """ - DADO: Un agente con herramientas - CUANDO: Se accede a la propiedad tools - ENTONCES: Retorna una copia de la lista de herramientas - """ - tools_copy = agent_with_tool.tools - - assert len(tools_copy) == 1 - assert tools_copy[0].name == "calculator" - - # Modificar la copia no afecta al agente - tools_copy.clear() - assert len(agent_with_tool.tools) == 1 - - def test_multiple_tools_independently(self, mock_model, default_config): - """ - DADO: Un agente con múltiples herramientas diferentes - CUANDO: Se ejecutan herramientas - ENTONCES: Cada una se ejecuta de forma independiente - """ - weather_tool = Mock() - weather_tool.name = "get_weather" - weather_tool.invoke = Mock(return_value="Sunny 25°C") - - time_tool = Mock() - time_tool.name = "get_time" - time_tool.invoke = Mock(return_value="14:30") - - agent = Agent( - model=mock_model, - config=default_config, - tools=[weather_tool, time_tool] - ) - - # Ejecutar weather_tool - result1 = agent._execute_tool({ - "name": "get_weather", - "id": "w1", - "args": {} - }) - assert "Sunny" in result1.content - - # Ejecutar time_tool - result2 = agent._execute_tool({ - "name": "get_time", - "id": "t1", - "args": {} - }) - assert "14:30" in result2.content - - -# ============================================================================ -# TESTS DE EDGE CASES -# ============================================================================ - - -class TestToolsEdgeCases: - """Pruebas de casos extremos y edge cases.""" - - def test_tool_returns_none(self, mock_model, default_config): - """ - DADO: Una herramienta que retorna None - CUANDO: Se ejecuta - ENTONCES: Se convierte a string adecuadamente - """ - tool = Mock() - tool.name = "none_tool" - tool.invoke = Mock(return_value=None) - - agent = Agent( - model=mock_model, - config=default_config, - tools=[tool] - ) - - result = agent._execute_tool({ - "name": "none_tool", - "id": "n1", - "args": {} - }) - - assert result.content == "None" - assert result.status == "success" - - def test_tool_returns_complex_object(self, mock_model, default_config): - """ - DADO: Una herramienta que retorna objeto complejo - CUANDO: Se ejecuta - ENTONCES: Se serializa a string correctamente - """ - tool = Mock() - tool.name = "complex_tool" - tool.invoke = Mock(return_value={"key": "value", "nested": {"data": 123}}) - - agent = Agent( - model=mock_model, - config=default_config, - tools=[tool] - ) - - result = agent._execute_tool({ - "name": "complex_tool", - "id": "c1", - "args": {} - }) - - assert isinstance(result.content, str) - assert "key" in result.content - assert "value" in result.content - - def test_empty_tools_list(self, mock_model, default_config): - """ - DADO: Un agente sin herramientas - CUANDO: Se intenta ejecutar una herramienta - ENTONCES: Retorna error apropiado - """ - agent = Agent( - model=mock_model, - config=default_config, - tools=[] - ) - - result = agent._execute_tool({ - "name": "any_tool", - "id": "e1", - "args": {} - }) - - assert result.status == "error" - assert "not found" in result.content.lower() - - def test_tool_with_special_characters_in_response(self, mock_model, default_config): - """ - DADO: Una herramienta que retorna strings especiales - CUANDO: Se ejecuta - ENTONCES: Se preservan los caracteres especiales - """ - tool = Mock() - tool.name = "special_tool" - tool.invoke = Mock(return_value="Response with special chars: ñ, é, 中文, 🚀") - - agent = Agent( - model=mock_model, - config=default_config, - tools=[tool] - ) - - result = agent._execute_tool({ - "name": "special_tool", - "id": "s1", - "args": {} - }) - - assert "ñ" in result.content - assert "中文" in result.content - - def test_tool_call_missing_name(self, agent): - """ - DADO: Un tool_call sin campo 'name' - CUANDO: Se ejecuta - ENTONCES: Maneja el error gracefully - """ - result = agent._execute_tool({ - "id": "m1", - "args": {} - # Sin 'name' - }) - - assert result.status == "error" - - def test_tool_call_empty_args_dict(self, agent_with_tool): - """ - DADO: Un tool_call con args vacío - CUANDO: Se ejecuta - ENTONCES: Invoca la herramienta con dict vacío - """ - agent_with_tool._tools[0].invoke = Mock(return_value="Empty args result") - - result = agent_with_tool._execute_tool({ - "name": "calculator", - "id": "ea1", - "args": {} - }) - - agent_with_tool._tools[0].invoke.assert_called_with({}) - assert result.status == "success" diff --git a/tests/test_tool_registry.py b/tests/test_tool_registry.py deleted file mode 100644 index dfc5614..0000000 --- a/tests/test_tool_registry.py +++ /dev/null @@ -1,434 +0,0 @@ -""" -Tests para el patrón Registry de herramientas. - -Prueba la funcionalidad del ToolRegistry y la integración con el Agent. -""" - -import pytest -from src.naliiabot.bot.tools.tool_registry import ( - ToolRegistry, - ToolRegistryBuilder, - BaseTool -) -from src.naliiabot.bot.tools.tools import ( - CalculatorTool, - GreeterTool, - WeatherTool, - TimeTool -) -from src.naliiabot.bot.agent.agent import Agent, AgentConfig - - -# ============================================================================ -# TESTS DEL BASETOOL -# ============================================================================ - - -class TestBaseTool: - """Pruebas para la clase base BaseTool.""" - - def test_calculator_tool_properties(self): - """Verificar que CalculatorTool implementa correctamente BaseTool.""" - tool = CalculatorTool() - - assert tool.name == "calculator" - assert "mathematical" in tool.description.lower() - assert "properties" in tool.args_schema - assert "required" in tool.args_schema - - def test_calculator_add(self): - """Prueba la operación de suma.""" - tool = CalculatorTool() - result = tool.invoke(a=5, b=3, operation="add") - - assert "8" in result - assert "add" in result - - def test_calculator_subtract(self): - """Prueba la operación de resta.""" - tool = CalculatorTool() - result = tool.invoke(a=10, b=4, operation="subtract") - - assert "6" in result - - def test_calculator_multiply(self): - """Prueba la operación de multiplicación.""" - tool = CalculatorTool() - result = tool.invoke(a=6, b=7, operation="multiply") - - assert "42" in result - - def test_calculator_divide(self): - """Prueba la operación de división.""" - tool = CalculatorTool() - result = tool.invoke(a=20, b=4, operation="divide") - - assert "5" in result - - def test_calculator_divide_by_zero(self): - """Prueba el manejo de división por cero.""" - tool = CalculatorTool() - result = tool.invoke(a=10, b=0, operation="divide") - - assert "Error" in result or "zero" in result.lower() - - def test_greeter_tool_formal(self): - """Prueba el saludo formal.""" - tool = GreeterTool() - result = tool.invoke(name="Alice", tone="formal") - - assert "Alice" in result - assert "pleasure" in result.lower() - - def test_greeter_tool_casual(self): - """Prueba el saludo casual.""" - tool = GreeterTool() - result = tool.invoke(name="Bob", tone="casual") - - assert "Bob" in result - assert "up" in result.lower() - - def test_greeter_tool_friendly(self): - """Prueba el saludo amigable.""" - tool = GreeterTool() - result = tool.invoke(name="Charlie", tone="friendly") - - assert "Charlie" in result - - def test_weather_tool(self): - """Prueba la herramienta de clima.""" - tool = WeatherTool() - result = tool.invoke(location="Madrid", units="celsius") - - assert "Madrid" in result - assert "Weather" in result or "temperature" in result.lower() - - def test_time_tool_24h_format(self): - """Prueba la herramienta de hora en formato 24h.""" - tool = TimeTool() - result = tool.invoke(format="24h", timezone="UTC") - - assert "UTC" in result - assert ":" in result # Contiene al menos un separador de hora - - def test_time_tool_12h_format(self): - """Prueba la herramienta de hora en formato 12h.""" - tool = TimeTool() - result = tool.invoke(format="12h", timezone="EST") - - assert "EST" in result - assert ("AM" in result or "PM" in result) - - -# ============================================================================ -# TESTS DEL TOOL REGISTRY -# ============================================================================ - - -class TestToolRegistry: - """Pruebas para el ToolRegistry.""" - - def test_registry_empty_on_init(self): - """Verificar que el registro inicia vacío.""" - registry = ToolRegistry() - - assert len(registry) == 0 - assert registry.list_tools() == {} - - def test_register_single_tool(self): - """Registrar una sola herramienta.""" - registry = ToolRegistry() - tool = CalculatorTool() - - registry.register(tool) - - assert len(registry) == 1 - assert registry.has_tool("calculator") - assert registry.get_tool("calculator") == tool - - def test_register_multiple_tools(self): - """Registrar múltiples herramientas.""" - registry = ToolRegistry() - tools = [CalculatorTool(), GreeterTool(), WeatherTool()] - - registry.register_multiple(tools) - - assert len(registry) == 3 - assert registry.has_tool("calculator") - assert registry.has_tool("greeter") - assert registry.has_tool("get_weather") - - def test_register_duplicate_tool_raises_error(self): - """Intentar registrar una herramienta duplicada debe fallar.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - - with pytest.raises(ValueError, match="already registered"): - registry.register(CalculatorTool()) - - def test_register_non_basetool_raises_error(self): - """Registrar algo que no es BaseTool debe fallar.""" - registry = ToolRegistry() - - with pytest.raises(TypeError, match="must be instance of BaseTool"): - registry.register("not a tool") - - def test_get_tool_existing(self): - """Obtener una herramienta existente.""" - registry = ToolRegistry() - tool = CalculatorTool() - registry.register(tool) - - retrieved = registry.get_tool("calculator") - - assert retrieved == tool - - def test_get_tool_non_existing(self): - """Obtener una herramienta que no existe retorna None.""" - registry = ToolRegistry() - - result = registry.get_tool("nonexistent") - - assert result is None - - def test_get_all_tools(self): - """Obtener todas las herramientas.""" - registry = ToolRegistry() - tools = [CalculatorTool(), GreeterTool()] - registry.register_multiple(tools) - - all_tools = registry.get_all_tools() - - assert len(all_tools) == 2 - assert all(isinstance(t, BaseTool) for t in all_tools) - - def test_unregister_tool(self): - """Desregistrar una herramienta.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - - registry.unregister("calculator") - - assert len(registry) == 0 - assert not registry.has_tool("calculator") - - def test_unregister_non_existing_raises_error(self): - """Desregistrar una herramienta que no existe debe fallar.""" - registry = ToolRegistry() - - with pytest.raises(KeyError, match="not found"): - registry.unregister("nonexistent") - - def test_list_tools(self): - """Listar todas las herramientas con descripciones.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - registry.register(GreeterTool()) - - tools_list = registry.list_tools() - - assert isinstance(tools_list, dict) - assert "calculator" in tools_list - assert "greeter" in tools_list - assert len(tools_list) == 2 - - def test_clear_registry(self): - """Limpiar el registro.""" - registry = ToolRegistry() - registry.register_multiple([ - CalculatorTool(), - GreeterTool(), - WeatherTool() - ]) - - registry.clear() - - assert len(registry) == 0 - - def test_contains_operator(self): - """Usar el operador 'in' para verificar existencia.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - - assert "calculator" in registry - assert "greeter" not in registry - - def test_get_langchain_tools(self): - """Obtener herramientas en formato LangChain.""" - registry = ToolRegistry() - registry.register_multiple([ - CalculatorTool(), - GreeterTool() - ]) - - langchain_tools = registry.get_langchain_tools() - - assert len(langchain_tools) == 2 - assert all(hasattr(t, 'name') for t in langchain_tools) - assert all(hasattr(t, 'description') for t in langchain_tools) - - def test_registry_repr(self): - """Prueba la representación en string del registro.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - - repr_str = repr(registry) - - assert "ToolRegistry" in repr_str - assert "calculator" in repr_str - - -# ============================================================================ -# TESTS DEL TOOL REGISTRY BUILDER -# ============================================================================ - - -class TestToolRegistryBuilder: - """Pruebas para el ToolRegistryBuilder.""" - - def test_builder_fluent_interface(self): - """Verificar la interfaz fluida del builder.""" - registry = (ToolRegistryBuilder() - .add(CalculatorTool()) - .add(GreeterTool()) - .build()) - - assert isinstance(registry, ToolRegistry) - assert len(registry) == 2 - - def test_builder_add_multiple(self): - """Añadir múltiples herramientas con builder.""" - registry = (ToolRegistryBuilder() - .add_multiple([ - CalculatorTool(), - GreeterTool(), - WeatherTool() - ]) - .build()) - - assert len(registry) == 3 - - def test_builder_mixed_operations(self): - """Mezclar operaciones add y add_multiple.""" - registry = (ToolRegistryBuilder() - .add(CalculatorTool()) - .add_multiple([GreeterTool(), WeatherTool()]) - .add(TimeTool()) - .build()) - - assert len(registry) == 4 - assert registry.has_tool("calculator") - assert registry.has_tool("greeter") - assert registry.has_tool("get_weather") - assert registry.has_tool("get_time") - - -# ============================================================================ -# TESTS DE INTEGRACIÓN CON AGENT -# ============================================================================ - - -class TestAgentWithRegistry: - """Pruebas de integración del Agent con ToolRegistry.""" - - def test_agent_accepts_tool_registry(self, mock_model): - """El agente debe aceptar un ToolRegistry.""" - registry = ToolRegistry() - registry.register_multiple([ - CalculatorTool(), - GreeterTool() - ]) - - agent = Agent(model=mock_model, tools=registry) - - assert len(agent.tools) == 2 - - def test_agent_registry_list_conversion(self, mock_model): - """El agente debe convertir ToolRegistry a lista internamente.""" - registry = ToolRegistry() - registry.register(CalculatorTool()) - - agent = Agent(model=mock_model, tools=registry) - - # Internamente debe tener la lista de tools - assert len(agent._tools) == 1 - - def test_agent_with_builder_registry(self, mock_model): - """Usar Agent con ToolRegistry creado con Builder.""" - registry = (ToolRegistryBuilder() - .add(CalculatorTool()) - .add(WeatherTool()) - .build()) - - agent = Agent(model=mock_model, tools=registry) - - assert len(agent.tools) == 2 - - def test_agent_still_accepts_list_of_tools(self, mock_model): - """El agente debe seguir aceptando listas de tools.""" - tools = [CalculatorTool(), GreeterTool()] - - agent = Agent(model=mock_model, tools=tools) - - assert len(agent.tools) == 2 - - def test_agent_none_tools_default(self, mock_model): - """El agente debe funcionar sin tools.""" - agent = Agent(model=mock_model, tools=None) - - assert len(agent.tools) == 0 - - -# ============================================================================ -# TESTS DE CASOS DE USO PRÁCTICOS -# ============================================================================ - - -class TestPracticalUseCases: - """Casos de uso prácticos del ToolRegistry.""" - - def test_build_customer_service_registry(self): - """Construir un registro para un agente de servicio al cliente.""" - registry = (ToolRegistryBuilder() - .add(GreeterTool()) - .add(WeatherTool()) - .add(TimeTool()) - .build()) - - assert registry.has_tool("greeter") - assert len(registry) == 3 - - def test_dynamic_tool_addition(self): - """Añadir herramientas dinámicamente.""" - registry = ToolRegistry() - - # Fase 1: registrar herramientas básicas - registry.register(CalculatorTool()) - assert len(registry) == 1 - - # Fase 2: registrar herramientas adicionales - registry.register(GreeterTool()) - assert len(registry) == 2 - - # Fase 3: remover si es necesario - registry.unregister("calculator") - assert len(registry) == 1 - - def test_tool_registry_documentation(self): - """Generar documentación de herramientas disponibles.""" - registry = (ToolRegistryBuilder() - .add_multiple([ - CalculatorTool(), - GreeterTool(), - WeatherTool(), - TimeTool() - ]) - .build()) - - tools_doc = registry.list_tools() - - assert len(tools_doc) == 4 - for tool_name, description in tools_doc.items(): - assert isinstance(tool_name, str) - assert isinstance(description, str) - assert len(description) > 0