""" 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