forked from HAL9000/cleveragents-core
2287 lines
76 KiB
Python
2287 lines
76 KiB
Python
"""
|
|
Unit tests for core/application.py
|
|
|
|
Tests the ReactiveCleverAgentsApp class and its methods.
|
|
"""
|
|
|
|
import asyncio
|
|
import io
|
|
import json
|
|
import logging
|
|
import pytest
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import Mock, patch, AsyncMock
|
|
|
|
from cleveragents.agents.factory import AgentFactory
|
|
from cleveragents.agents.tool import ToolAgent
|
|
from cleveragents.core.application import ReactiveCleverAgentsApp
|
|
from cleveragents.core.exceptions import (
|
|
AgentCreationError,
|
|
UnsafeConfigurationError,
|
|
CleverAgentsException
|
|
)
|
|
from cleveragents.langgraph.graph import GraphConfig
|
|
from cleveragents.reactive.route import RouteType
|
|
from cleveragents.reactive.stream_router import StreamMessage, StreamType
|
|
from cleveragents.templates.renderer import TemplateRenderer, TemplateEngine
|
|
|
|
|
|
# Helper function to get fixture file path
|
|
def get_fixture_path(filename: str) -> Path:
|
|
"""Get path to a test fixture file."""
|
|
return Path(__file__).parent.parent.parent / "fixtures" / filename
|
|
|
|
|
|
class TestReactiveCleverAgentsAppInitialization:
|
|
"""Test suite for ReactiveCleverAgentsApp initialization."""
|
|
|
|
def test_initialization_without_config(self):
|
|
"""Test basic initialization without config files."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
assert app is not None
|
|
assert app.config is None
|
|
assert app.agents == {}
|
|
assert app.results == []
|
|
assert app.errors == []
|
|
assert app.unsafe is False
|
|
assert app.verbose is False
|
|
|
|
def test_initialization_with_verbose(self):
|
|
"""Test initialization with verbose logging."""
|
|
app = ReactiveCleverAgentsApp(verbose=True)
|
|
|
|
assert app.verbose is True
|
|
|
|
def test_initialization_with_unsafe(self):
|
|
"""Test initialization with unsafe mode."""
|
|
app = ReactiveCleverAgentsApp(unsafe=True)
|
|
|
|
assert app.unsafe is True
|
|
|
|
def test_initialization_creates_stream_router(self):
|
|
"""Test that initialization creates a stream router."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
assert app.stream_router is not None
|
|
assert hasattr(app, 'scheduler')
|
|
|
|
def test_initialization_creates_langgraph_bridge(self):
|
|
"""Test that initialization creates a LangGraph bridge."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
assert app.langgraph_bridge is not None
|
|
|
|
def test_initialization_creates_config_parser(self):
|
|
"""Test that initialization creates a config parser."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
assert app.config_parser is not None
|
|
|
|
def test_initialization_with_all_flags_enabled(self):
|
|
"""Test initialization with all flags enabled."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, verbose=True, unsafe=True)
|
|
|
|
assert app.verbose is True
|
|
assert app.unsafe is True
|
|
assert app.stream_router is not None
|
|
|
|
def test_initialization_with_all_flags_disabled(self):
|
|
"""Test initialization with all flags disabled."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, verbose=False, unsafe=False)
|
|
|
|
assert app.verbose is False
|
|
assert app.unsafe is False
|
|
|
|
|
|
class TestEnforceUnsafeFlag:
|
|
"""Test suite for _enforce_unsafe_flag method."""
|
|
|
|
def test_enforce_unsafe_flag_with_unsafe_config_and_flag(self):
|
|
"""Test that unsafe config with unsafe flag does not raise."""
|
|
app = ReactiveCleverAgentsApp(unsafe=True)
|
|
app.config = Mock()
|
|
app.config.global_context = {"unsafe": True}
|
|
|
|
# Should not raise
|
|
app._enforce_unsafe_flag()
|
|
|
|
def test_enforce_unsafe_flag_with_unsafe_config_without_flag(self):
|
|
"""Test that unsafe config without unsafe flag raises error."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
app.config = Mock()
|
|
app.config.global_context = {"unsafe": True}
|
|
|
|
with pytest.raises(UnsafeConfigurationError) as exc_info:
|
|
app._enforce_unsafe_flag()
|
|
|
|
assert "'--unsafe'" in str(exc_info.value)
|
|
|
|
def test_enforce_unsafe_flag_with_safe_config(self):
|
|
"""Test that safe config does not raise regardless of flag."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
app.config = Mock()
|
|
app.config.global_context = {"unsafe": False}
|
|
|
|
# Should not raise
|
|
app._enforce_unsafe_flag()
|
|
|
|
def test_enforce_unsafe_flag_with_no_config(self):
|
|
"""Test that no config does not raise."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
app.config = None
|
|
|
|
# Should not raise
|
|
app._enforce_unsafe_flag()
|
|
|
|
def test_enforce_unsafe_flag_with_missing_unsafe_key(self):
|
|
"""Test that missing unsafe key in config does not raise."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
app.config = Mock()
|
|
app.config.global_context = {}
|
|
|
|
# Should not raise
|
|
app._enforce_unsafe_flag()
|
|
|
|
|
|
class TestSanitizeJsonString:
|
|
"""Test suite for _sanitize_json_string static method."""
|
|
|
|
def test_sanitize_json_valid(self):
|
|
"""Test that valid JSON is not modified."""
|
|
valid_json = '{"key": "value", "number": 42}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(valid_json)
|
|
|
|
assert result == valid_json
|
|
parsed = json.loads(result)
|
|
assert parsed["key"] == "value"
|
|
assert parsed["number"] == 42
|
|
|
|
def test_sanitize_json_with_newlines(self):
|
|
"""Test that newlines are properly escaped."""
|
|
json_with_newlines = '{"content": "Line 1\nLine 2\nLine 3"}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(json_with_newlines)
|
|
|
|
# Should parse without error
|
|
parsed = json.loads(result)
|
|
assert "Line 1" in parsed["content"]
|
|
assert "Line 2" in parsed["content"]
|
|
|
|
def test_sanitize_json_with_tabs(self):
|
|
"""Test that tabs are properly escaped."""
|
|
json_with_tabs = '{"content": "Column1\tColumn2\tColumn3"}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(json_with_tabs)
|
|
|
|
# Should parse without error
|
|
parsed = json.loads(result)
|
|
assert "Column1" in parsed["content"]
|
|
|
|
def test_sanitize_json_empty_string(self):
|
|
"""Test that empty string returns empty string."""
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string("")
|
|
assert result == ""
|
|
|
|
def test_sanitize_json_with_unicode(self):
|
|
"""Test handling of unicode characters."""
|
|
json_with_unicode = '{"message": "Hello 世界 🌍"}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(json_with_unicode)
|
|
|
|
parsed = json.loads(result)
|
|
assert "世界" in parsed["message"]
|
|
assert "🌍" in parsed["message"]
|
|
|
|
|
|
class TestConfigToDict:
|
|
"""Test suite for _config_to_dict method."""
|
|
|
|
def test_config_to_dict_with_none_config(self):
|
|
"""Test _config_to_dict when config is None."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
result = app._config_to_dict()
|
|
|
|
assert result == {}
|
|
|
|
def test_config_to_dict_with_reactive_config(self):
|
|
"""Test _config_to_dict with ReactiveConfig."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create a mock agent config object with proper attributes
|
|
mock_agent_config = Mock()
|
|
mock_agent_config.type = "llm"
|
|
mock_agent_config.name = "test_agent"
|
|
mock_agent_config.model = "gpt-4"
|
|
mock_agent_config.parameters = {}
|
|
|
|
# Create a mock config with the expected structure
|
|
mock_config = Mock()
|
|
mock_config.agents = {"test_agent": mock_agent_config}
|
|
mock_config.routes = {}
|
|
mock_config.streams = {}
|
|
mock_config.graphs = {}
|
|
mock_config.templates = {}
|
|
mock_config.global_context = {}
|
|
|
|
app.config = mock_config
|
|
|
|
result = app._config_to_dict()
|
|
|
|
assert isinstance(result, dict)
|
|
assert "agents" in result
|
|
|
|
|
|
class TestDispose:
|
|
"""Test suite for dispose method."""
|
|
|
|
def test_dispose_calls_stream_router_dispose(self):
|
|
"""Test that dispose calls stream_router.dispose()."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Mock the stream_router's dispose method
|
|
app.stream_router.dispose = Mock()
|
|
|
|
app.dispose()
|
|
|
|
app.stream_router.dispose.assert_called_once()
|
|
|
|
def test_dispose_handles_no_stream_router(self):
|
|
"""Test that dispose handles case when stream_router is None."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.stream_router = None
|
|
|
|
# Should not raise
|
|
app.dispose()
|
|
|
|
|
|
class TestPrintHelp:
|
|
"""Test suite for _print_help method."""
|
|
|
|
def test_print_help_outputs_help_text(self, capsys):
|
|
"""Test that _print_help outputs help text."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
app._print_help()
|
|
|
|
captured = capsys.readouterr()
|
|
# Check that help text contains expected content
|
|
assert "Available commands:" in captured.out
|
|
|
|
|
|
class TestProcessToolCommands:
|
|
"""Test suite for _process_tool_commands method."""
|
|
|
|
def test_process_tool_commands_no_commands(self):
|
|
"""Test processing content with no tool commands."""
|
|
app = ReactiveCleverAgentsApp()
|
|
content = "This is just regular text without tool commands."
|
|
|
|
result = app._process_tool_commands(content)
|
|
|
|
assert result == content
|
|
|
|
def test_process_tool_commands_with_simple_command(self):
|
|
"""Test processing content with a simple tool command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app._execute_single_tool = Mock(return_value="executed result")
|
|
|
|
content = "[TOOL_EXECUTE:test_tool]\n{}\n[/TOOL_EXECUTE]"
|
|
|
|
result = app._process_tool_commands(content)
|
|
|
|
# Should contain the result
|
|
assert "executed result" in result
|
|
|
|
def test_process_tool_commands_preserves_surrounding_text(self):
|
|
"""Test that surrounding text is preserved."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app._execute_single_tool = Mock(return_value="result")
|
|
|
|
content = "Before\n[TOOL_EXECUTE:tool]\n{}\n[/TOOL_EXECUTE]\nAfter"
|
|
|
|
result = app._process_tool_commands(content)
|
|
|
|
assert "Before" in result
|
|
assert "After" in result
|
|
|
|
def test_process_tool_commands_with_json_decode_error(self):
|
|
"""Test process tool commands with invalid JSON."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
content = "[TOOL_EXECUTE:test_tool]\n{invalid json here}\n[/TOOL_EXECUTE]"
|
|
|
|
result = app._process_tool_commands(content)
|
|
|
|
assert "❌ Error: Invalid tool parameters format" in result
|
|
|
|
|
|
class TestExecuteSingleTool:
|
|
"""Test suite for _execute_single_tool method."""
|
|
|
|
def test_execute_single_tool_with_safe_mode(self):
|
|
"""Test tool execution in safe mode."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
|
|
# In safe mode, should return an error or limited execution
|
|
result = app._execute_single_tool("unknown_tool", {})
|
|
|
|
assert isinstance(result, str)
|
|
# Should indicate tool not found
|
|
assert "Error: No agent available to execute tool 'unknown_tool'" in result
|
|
|
|
def test_execute_single_tool_with_none_params(self):
|
|
"""Test tool execution with None params."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
result = app._execute_single_tool("test_tool", None)
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_execute_single_tool_with_tool_in_agent(self):
|
|
"""Test executing tool that exists in agent.tools."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=True)
|
|
|
|
mock_config = {"tools": ["echo", "math"]}
|
|
mock_renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
|
|
tool_agent = ToolAgent("test_tool_agent", mock_config, mock_renderer)
|
|
app.agents = {"test_tool_agent": tool_agent}
|
|
|
|
with patch.object(tool_agent, 'process_message', new=AsyncMock(return_value="echo result")):
|
|
result = app._execute_single_tool("echo", {"text": "test"})
|
|
|
|
assert "✅ echo result" in result
|
|
|
|
|
|
class TestVisualizeNetwork:
|
|
"""Test suite for visualize_network method."""
|
|
|
|
def test_visualize_network_mermaid_format(self):
|
|
"""Test network visualization in Mermaid format."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
result = app.visualize_network(output_format="mermaid")
|
|
|
|
assert isinstance(result, str)
|
|
# Mermaid diagrams start with "graph TD"
|
|
assert "graph TD" in result
|
|
|
|
def test_visualize_network_default_format(self):
|
|
"""Test network visualization with default format."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
result = app.visualize_network()
|
|
|
|
assert isinstance(result, str)
|
|
|
|
def test_visualize_network_with_no_config(self):
|
|
"""Test visualization when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
result = app.visualize_network()
|
|
|
|
assert isinstance(result, str)
|
|
# Should handle empty/no config gracefully
|
|
|
|
def test_visualize_network_unsupported_format(self):
|
|
"""Test visualization with unsupported format returns error message."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
result = app.visualize_network(output_format="unknown_format")
|
|
|
|
# Should indicate format not supported
|
|
assert "not supported" in result.lower()
|
|
|
|
def test_visualize_network_empty_config(self):
|
|
"""Test network visualization with empty/no configuration."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
app.config = Mock()
|
|
app.config.routes = {}
|
|
app.config.merges = []
|
|
app.agents = {}
|
|
|
|
result = app.visualize_network(output_format="mermaid")
|
|
|
|
# Should return valid mermaid syntax even with empty config
|
|
assert "graph TD" in result
|
|
|
|
def test_visualize_network_with_merges(self):
|
|
"""Test visualization with merge configurations."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
app.agents = {}
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {}
|
|
mock_config.merges = [
|
|
{"sources": ["stream1", "stream2"], "target": "merged"}
|
|
]
|
|
app.config = mock_config
|
|
|
|
result = app.visualize_network("mermaid")
|
|
|
|
# All merge components should be in the visualization
|
|
assert "stream1" in result
|
|
assert "stream2" in result
|
|
assert "merged" in result
|
|
|
|
def test_visualize_network_with_langgraphs(self):
|
|
"""Test visualization with LangGraphs."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
app.agents = {}
|
|
|
|
app.langgraph_bridge.list_graphs = Mock(return_value=["graph1", "graph2"])
|
|
|
|
mock_graph = Mock()
|
|
mock_graph.visualize = Mock(return_value=" node1[Node 1]\n node2[Node 2]")
|
|
app.langgraph_bridge.get_graph = Mock(return_value=mock_graph)
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {}
|
|
mock_config.merges = []
|
|
app.config = mock_config
|
|
|
|
result = app.visualize_network("mermaid")
|
|
|
|
# Both graphs should be in subgraphs
|
|
assert "subgraph graph1" in result
|
|
assert "subgraph graph2" in result
|
|
|
|
|
|
class TestHandleStreamCommand:
|
|
"""Test suite for _handle_stream_command method."""
|
|
|
|
def test_handle_stream_command_help(self, capsys):
|
|
"""Test handling help command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
app._handle_stream_command("help")
|
|
|
|
captured = capsys.readouterr()
|
|
# Should output usage message when command format is wrong
|
|
assert "Usage: /stream <name> <message>" in captured.out
|
|
|
|
def test_handle_stream_command_quit(self):
|
|
"""Test handling quit command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Should not raise, might exit or set a flag
|
|
try:
|
|
app._handle_stream_command("quit")
|
|
except SystemExit:
|
|
# Quit might call sys.exit()
|
|
pass
|
|
|
|
def test_handle_stream_command_unknown(self, capsys):
|
|
"""Test handling unknown command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Single word without space triggers usage message
|
|
app._handle_stream_command("unknown_command_xyz")
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Usage: /stream <name> <message>" in captured.out
|
|
|
|
def test_handle_stream_command_visualize(self):
|
|
"""Test handling visualize command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Should not raise
|
|
app._handle_stream_command("visualize")
|
|
|
|
def test_handle_stream_command_invalid_format(self):
|
|
"""Test stream command with missing message prints usage."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
# Invalid format - only stream name, no message
|
|
app._handle_stream_command("stream_name")
|
|
output = captured.getvalue()
|
|
|
|
# Should print usage message
|
|
assert "Usage" in output
|
|
assert "/stream" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
def test_handle_stream_command_stream_not_found(self):
|
|
"""Test stream command with non-existent stream prints error."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
# Stream doesn't exist
|
|
app._handle_stream_command("nonexistent test_message")
|
|
output = captured.getvalue()
|
|
|
|
# Should print stream not found error
|
|
assert "not found" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
def test_handle_stream_command_with_valid_stream_in_config(self):
|
|
"""Test stream command with valid stream in config."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
app.config = Mock()
|
|
app.config.global_context = {}
|
|
|
|
# Mock a stream in the router
|
|
app.stream_router.streams["test_stream"] = Mock()
|
|
app.stream_router.send_message = Mock()
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
app._handle_stream_command("test_stream hello")
|
|
output = captured.getvalue()
|
|
|
|
# Should confirm message sent
|
|
assert "Message sent to stream 'test_stream'" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
def test_handle_stream_command_with_stream_in_router(self, capsys):
|
|
"""Test handle stream command where stream_name in self.stream_router.streams."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
app.stream_router.streams["my_stream"] = Mock()
|
|
app.stream_router.send_message = Mock()
|
|
|
|
app._handle_stream_command("my_stream test message")
|
|
|
|
assert app.stream_router.send_message.called
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Message sent to stream 'my_stream'" in captured.out
|
|
|
|
|
|
class TestLoadConfiguration:
|
|
"""Test suite for load_configuration method."""
|
|
|
|
@pytest.fixture
|
|
def temp_config_file(self):
|
|
"""Create a temporary valid config file."""
|
|
with tempfile.NamedTemporaryFile(mode='w', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
f.write("""
|
|
version: "1.0"
|
|
|
|
agents:
|
|
test_agent:
|
|
type: llm
|
|
model: gpt-4
|
|
config:
|
|
temperature: 0.7
|
|
|
|
routes:
|
|
main_route:
|
|
type: reactive
|
|
source: input
|
|
target: test_agent
|
|
|
|
streams:
|
|
input:
|
|
operators: []
|
|
|
|
cleveragents:
|
|
default_router: main_route
|
|
""")
|
|
temp_path = Path(f.name)
|
|
|
|
yield temp_path
|
|
|
|
# Cleanup
|
|
if temp_path.exists():
|
|
temp_path.unlink()
|
|
|
|
def test_load_configuration_basic(self, temp_config_file):
|
|
"""Test loading a basic configuration file."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
try:
|
|
app.load_configuration([temp_config_file])
|
|
|
|
# Config should be loaded
|
|
assert app.config is not None
|
|
except Exception as e:
|
|
# Configuration loading might fail due to missing dependencies
|
|
# but the method should at least attempt to load
|
|
assert "config" in str(e).lower() or isinstance(e, (CleverAgentsException, Exception))
|
|
|
|
def test_load_configuration_with_nonexistent_file(self):
|
|
"""Test loading configuration with nonexistent file."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
nonexistent_path = Path("/nonexistent/path/config.yaml")
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
app.load_configuration([nonexistent_path])
|
|
|
|
# Should mention the file that failed to load
|
|
assert "config.yaml" in str(exc_info.value)
|
|
|
|
def test_load_configuration_with_multiple_files(self, temp_config_file):
|
|
"""Test loading multiple configuration files."""
|
|
temp_path_2 = get_fixture_path("additional_agents.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
try:
|
|
app.load_configuration([temp_config_file, temp_path_2])
|
|
|
|
# Config should be loaded and merged
|
|
assert app.config is not None
|
|
except Exception:
|
|
# Configuration loading might fail, but method should execute
|
|
pass
|
|
|
|
|
|
class TestRegisterTemplates:
|
|
"""Test suite for _register_templates method."""
|
|
|
|
def test_register_templates_with_no_config(self):
|
|
"""Test registering templates when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
# Should not raise
|
|
app._register_templates()
|
|
|
|
# Template registry might be None if no config is loaded, which is expected
|
|
# Just verify the method completes without error
|
|
|
|
def test_register_templates_with_empty_templates(self):
|
|
"""Test registering templates with empty templates config."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create mock config with empty templates
|
|
mock_config = Mock()
|
|
mock_config.templates = {}
|
|
mock_config.global_context = {}
|
|
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._register_templates()
|
|
|
|
def test_register_templates_with_non_dict_templates(self):
|
|
"""Test register templates handles non-dict template values."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_config = Mock()
|
|
mock_config.templates = {
|
|
"agents": "string_instead_of_dict",
|
|
"streams": {},
|
|
"graphs": {}
|
|
}
|
|
app.config = mock_config
|
|
|
|
# Should raise AttributeError when trying to iterate over string
|
|
with pytest.raises(AttributeError):
|
|
app._register_templates()
|
|
|
|
|
|
class TestCreateAgents:
|
|
"""Test suite for _create_agents method."""
|
|
|
|
def test_create_agents_with_no_config(self):
|
|
"""Test creating agents when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
# Should raise AgentCreationError when config is not initialized
|
|
with pytest.raises(AgentCreationError):
|
|
app._create_agents()
|
|
|
|
def test_create_agents_with_empty_agents_config(self):
|
|
"""Test creating agents with empty agents config."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create mock config with empty agents
|
|
mock_config = Mock()
|
|
mock_config.agents = {}
|
|
|
|
app.config = mock_config
|
|
# Create a proper mock for agent_factory
|
|
mock_factory = Mock()
|
|
mock_factory.get_agent_types = Mock(return_value=[])
|
|
app.agent_factory = mock_factory
|
|
|
|
# Should not raise with empty agents
|
|
app._create_agents()
|
|
|
|
# Agents dict should still be empty
|
|
assert app.agents == {}
|
|
|
|
def test_create_agents_with_template_instance_type(self):
|
|
"""Test creating agent with template_instance type."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_template_renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
app.agent_factory = AgentFactory({"agents": {}}, mock_template_renderer)
|
|
app.template_renderer = mock_template_renderer
|
|
|
|
mock_agent_config = Mock()
|
|
mock_agent_config.type = "template_instance"
|
|
mock_agent_config.config = {
|
|
"template": "my_template",
|
|
"params": {"param1": "value1"}
|
|
}
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {"test_agent": mock_agent_config}
|
|
app.config = mock_config
|
|
|
|
app.template_registry = Mock()
|
|
app.template_registry.instantiate_from_config = Mock(return_value={
|
|
"type": "llm",
|
|
"config": {"model": "gpt-4"}
|
|
})
|
|
app._use_enhanced_registry = False
|
|
|
|
mock_agent = Mock()
|
|
app.agent_factory.create_agent = Mock(return_value=mock_agent)
|
|
|
|
app._create_agents()
|
|
|
|
assert app.agent_factory.create_agent.called
|
|
|
|
|
|
class TestSetupRoutes:
|
|
"""Test suite for _setup_routes method."""
|
|
|
|
def test_setup_routes_with_no_config(self):
|
|
"""Test setting up routes when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
# Should not raise
|
|
app._setup_routes()
|
|
|
|
def test_setup_routes_with_empty_routes_config(self):
|
|
"""Test setting up routes with empty routes config."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create mock config with empty routes
|
|
mock_config = Mock()
|
|
mock_config.routes = {}
|
|
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_routes()
|
|
|
|
def test_setup_routes_with_template_config_having_template(self):
|
|
"""Test setup routes WITH template_config attribute."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_route = Mock()
|
|
mock_route.template_config = {"template": "my_template"}
|
|
mock_route.type = RouteType.STREAM
|
|
mock_route.stream_type = StreamType.COLD
|
|
mock_route.operators = []
|
|
mock_route.subscriptions = []
|
|
mock_route.publications = []
|
|
mock_route.agents = []
|
|
|
|
def mock_to_stream_config():
|
|
return {
|
|
"name": "test_stream",
|
|
"type": StreamType.COLD,
|
|
"operators": [],
|
|
"subscriptions": [],
|
|
"publications": []
|
|
}
|
|
|
|
mock_route.to_stream_config = mock_to_stream_config
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {"test_route": mock_route}
|
|
app.config = mock_config
|
|
|
|
app.template_registry = Mock()
|
|
app.template_registry.instantiate_from_config = Mock(return_value={
|
|
"type": "stream",
|
|
"stream_type": "cold",
|
|
"operators": [],
|
|
"subscriptions": [],
|
|
"publications": [],
|
|
"agents": []
|
|
})
|
|
|
|
app.stream_router.create_stream = Mock()
|
|
app._setup_routes()
|
|
|
|
app.template_registry.instantiate_from_config.assert_called()
|
|
|
|
def test_setup_routes_with_graph_type(self):
|
|
"""Test setup routes with GRAPH route type."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_route = Mock()
|
|
mock_route.type = RouteType.GRAPH
|
|
mock_route.nodes = {"start": Mock()}
|
|
mock_route.edges = []
|
|
mock_route.entry_point = "start"
|
|
mock_route.checkpointing = False
|
|
mock_route.state_class = None
|
|
mock_route.template_config = None
|
|
|
|
def mock_to_graph_config():
|
|
return GraphConfig(
|
|
name="test_graph",
|
|
nodes={},
|
|
edges=[],
|
|
entry_point="start"
|
|
)
|
|
|
|
mock_route.to_graph_config = mock_to_graph_config
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {"test_graph": mock_route}
|
|
app.config = mock_config
|
|
app.agents = {}
|
|
|
|
app._setup_routes()
|
|
|
|
assert "test_graph" in app.langgraph_bridge.graphs
|
|
|
|
def test_setup_routes_with_bridge_type(self):
|
|
"""Test setup routes with BRIDGE route type."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_route = Mock()
|
|
mock_route.type = RouteType.BRIDGE
|
|
mock_route.template_config = None
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {"test_bridge": mock_route}
|
|
app.config = mock_config
|
|
|
|
app._setup_routes()
|
|
|
|
assert app.config is not None
|
|
|
|
|
|
class TestSetupStreamOperations:
|
|
"""Test suite for _setup_stream_operations method."""
|
|
|
|
def test_setup_stream_operations_with_no_config(self):
|
|
"""Test setting up stream operations when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
# Should not raise
|
|
app._setup_stream_operations()
|
|
|
|
def test_setup_stream_operations_with_empty_streams(self):
|
|
"""Test setting up stream operations with empty streams."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create mock config with empty streams, merges, and splits
|
|
mock_config = Mock()
|
|
mock_config.streams = {}
|
|
mock_config.merges = [] # Make it iterable
|
|
mock_config.splits = [] # Make it iterable
|
|
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_stream_operations()
|
|
|
|
def test_setup_stream_operations_with_actual_merges(self):
|
|
"""Test stream operations with merge configuration."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_config = Mock()
|
|
mock_config.merges = [
|
|
{"sources": ["stream1", "stream2"], "target": "merged_stream"}
|
|
]
|
|
mock_config.splits = []
|
|
app.config = mock_config
|
|
|
|
app.stream_router.merge_streams = Mock()
|
|
app._setup_stream_operations()
|
|
|
|
app.stream_router.merge_streams.assert_called_once_with(
|
|
["stream1", "stream2"], "merged_stream"
|
|
)
|
|
|
|
def test_setup_stream_operations_with_actual_splits(self):
|
|
"""Test stream operations with split configuration."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
mock_config = Mock()
|
|
mock_config.merges = []
|
|
mock_config.splits = [
|
|
{
|
|
"source": "input_stream",
|
|
"targets": {
|
|
"output1": {"condition": "lambda x: x > 0"},
|
|
"output2": {"condition": "lambda x: x <= 0"}
|
|
}
|
|
}
|
|
]
|
|
app.config = mock_config
|
|
|
|
app.stream_router.split_stream = Mock()
|
|
app._setup_stream_operations()
|
|
|
|
assert app.stream_router.split_stream.called
|
|
|
|
|
|
class TestSetupPipelines:
|
|
"""Test suite for _setup_pipelines method."""
|
|
|
|
def test_setup_pipelines_with_no_config(self):
|
|
"""Test setting up pipelines when no config is loaded."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
# Should not raise
|
|
app._setup_pipelines()
|
|
|
|
def test_setup_pipelines_with_empty_pipelines(self):
|
|
"""Test setting up pipelines with empty pipelines."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Create mock config with empty pipelines (should be dict, not list)
|
|
mock_config = Mock()
|
|
mock_config.pipelines = {} # Changed from [] to {}
|
|
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_pipelines()
|
|
|
|
|
|
class TestLoadConfigurationWithMocking:
|
|
"""Test suite for load_configuration with proper mocking."""
|
|
|
|
@pytest.fixture
|
|
def temp_config_file(self):
|
|
"""Provide path to valid config file."""
|
|
return get_fixture_path("valid_config.yaml")
|
|
|
|
@pytest.fixture
|
|
def mock_reactive_config(self):
|
|
"""Create a comprehensive mock reactive config."""
|
|
|
|
mock_config = Mock()
|
|
mock_config.template_engine = "jinja2"
|
|
mock_config.prompts = {
|
|
"test_prompt": {"content": "Hello {{name}}"},
|
|
"simple_prompt": "Simple template"
|
|
}
|
|
mock_config.agents = {}
|
|
mock_config.routes = {}
|
|
mock_config.streams = {}
|
|
mock_config.merges = []
|
|
mock_config.splits = []
|
|
mock_config.graphs = {}
|
|
mock_config.templates = {}
|
|
mock_config.pipelines = {}
|
|
mock_config.global_context = {}
|
|
|
|
return mock_config
|
|
|
|
def _setup_mocked_app(self, mock_reactive_config):
|
|
"""Helper to create app with mocked config parser and setup methods."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config_parser.parse_files = Mock(return_value=mock_reactive_config)
|
|
app._register_templates = Mock()
|
|
app._create_agents = Mock()
|
|
app._setup_routes = Mock()
|
|
app._setup_stream_operations = Mock()
|
|
app._setup_pipelines = Mock()
|
|
return app
|
|
|
|
def test_load_configuration_with_mock(self, temp_config_file, mock_reactive_config):
|
|
"""Test loading configuration with mocked parser."""
|
|
app = self._setup_mocked_app(mock_reactive_config)
|
|
app.load_configuration([temp_config_file])
|
|
|
|
# Verify config was loaded successfully
|
|
assert app.config is not None
|
|
assert app.config == mock_reactive_config
|
|
assert app.template_renderer is not None
|
|
|
|
def test_load_configuration_calls_setup_methods(self, temp_config_file, mock_reactive_config):
|
|
"""Test that load_configuration calls all setup methods."""
|
|
app = self._setup_mocked_app(mock_reactive_config)
|
|
app.load_configuration([temp_config_file])
|
|
|
|
# Verify all setup methods were called in order
|
|
app._register_templates.assert_called_once()
|
|
app._create_agents.assert_called_once()
|
|
app._setup_routes.assert_called_once()
|
|
app._setup_stream_operations.assert_called_once()
|
|
app._setup_pipelines.assert_called_once()
|
|
|
|
def test_load_configuration_raises_on_parser_error(self):
|
|
"""Test that load_configuration raises CleverAgentsException on parser error."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Make parser raise an exception
|
|
app.config_parser.parse_files = Mock(side_effect=ValueError("Invalid config"))
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
app.load_configuration([Path("/fake/path.yaml")])
|
|
|
|
assert "Failed to load configuration" in str(exc_info.value)
|
|
|
|
|
|
class TestRunSingleShot:
|
|
"""Test suite for run_single_shot method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_requires_config(self):
|
|
"""Test that run_single_shot requires loaded configuration."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.run_single_shot("test prompt")
|
|
|
|
assert "Configuration not loaded" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_enforces_unsafe_flag(self):
|
|
"""Test that run_single_shot enforces unsafe flag."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
|
|
# Create mock config that requires unsafe mode
|
|
mock_config = Mock()
|
|
mock_config.global_context = {"unsafe": True}
|
|
app.config = mock_config
|
|
|
|
with pytest.raises(UnsafeConfigurationError):
|
|
await app.run_single_shot("test prompt")
|
|
|
|
|
|
class TestRunSingleShotExtended:
|
|
"""Extended test suite for run_single_shot method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_timeout(self):
|
|
"""Test run_single_shot with timeout."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Mock stream router
|
|
app.stream_router.send_message = Mock()
|
|
app.stream_router.subscribe_to_output = Mock()
|
|
|
|
# Test that timeout is handled
|
|
with pytest.raises((CleverAgentsException, asyncio.TimeoutError)):
|
|
await asyncio.wait_for(app.run_single_shot("test"), timeout=0.1)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_none_message(self):
|
|
"""Test run_single_shot handling None message in output."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Create a future that resolves with empty string
|
|
result_future = asyncio.Future()
|
|
result_future.set_result("")
|
|
|
|
with patch.object(app.stream_router, 'send_message'):
|
|
with patch.object(app.stream_router, 'subscribe_to_output') as mock_subscribe:
|
|
# Simulate output with None message
|
|
def fake_subscribe(observer):
|
|
observer.on_next(None)
|
|
mock_subscribe.side_effect = fake_subscribe
|
|
|
|
try:
|
|
result = await asyncio.wait_for(app.run_single_shot("test"), timeout=0.1)
|
|
assert result == ""
|
|
except asyncio.TimeoutError:
|
|
pass # Expected in some cases
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_generic_exception(self):
|
|
"""Test that generic exceptions are wrapped with context message."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# When a generic Exception is raised (not CleverAgentsException)
|
|
with patch.object(app.stream_router, 'send_message', side_effect=Exception("Test error")):
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.run_single_shot("test")
|
|
|
|
# Should wrap the error with context
|
|
error_message = str(exc_info.value)
|
|
assert "Failed to run in single-shot mode" in error_message
|
|
assert "Test error" in error_message
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_clever_exception(self):
|
|
"""Test that CleverAgentsException is re-raised as-is without wrapping."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# When a CleverAgentsException is raised
|
|
original_error = CleverAgentsException("Original error message")
|
|
with patch.object(app.stream_router, 'send_message', side_effect=original_error):
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.run_single_shot("test")
|
|
|
|
# Should re-raise as-is, NOT wrap it
|
|
error_message = str(exc_info.value)
|
|
assert error_message == "Original error message"
|
|
assert "Failed to run in single-shot mode" not in error_message
|
|
|
|
|
|
class TestStartInteractiveSession:
|
|
"""Test suite for start_interactive_session method."""
|
|
|
|
async def _setup_app_with_callback_capture(self):
|
|
"""
|
|
Helper method to set up app and capture callbacks from stream router.
|
|
|
|
Returns:
|
|
tuple: (app, output_callback, error_callback)
|
|
"""
|
|
app = ReactiveCleverAgentsApp(unsafe=True)
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Capture callbacks using closures
|
|
captured_output_callback = None
|
|
captured_error_callback = None
|
|
|
|
def capture_output_subscribe(observer):
|
|
nonlocal captured_output_callback
|
|
captured_output_callback = observer.on_next
|
|
|
|
def capture_error_subscribe(observer):
|
|
nonlocal captured_error_callback
|
|
captured_error_callback = observer.on_next
|
|
|
|
# Set up mock stream router
|
|
app.stream_router = Mock()
|
|
app.stream_router.subscribe_to_output = capture_output_subscribe
|
|
|
|
mock_error_observable = Mock()
|
|
mock_error_observable.subscribe = capture_error_subscribe
|
|
app.stream_router.observables = {"__error__": mock_error_observable}
|
|
|
|
# Mock input to exit immediately and run session
|
|
with patch('builtins.input', side_effect=['exit']):
|
|
await app.start_interactive_session()
|
|
|
|
return app, captured_output_callback, captured_error_callback
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interactive_session_requires_config(self):
|
|
"""Test that interactive session requires configuration."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.config = None
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.start_interactive_session()
|
|
|
|
assert "Configuration not loaded" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interactive_session_enforces_unsafe_flag(self):
|
|
"""Test that interactive session enforces unsafe flag."""
|
|
app = ReactiveCleverAgentsApp(unsafe=False)
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {"unsafe": True}
|
|
app.config = mock_config
|
|
|
|
with pytest.raises(UnsafeConfigurationError):
|
|
await app.start_interactive_session()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_output_with_normal_content(self, capsys):
|
|
"""Test on_output callback with normal content."""
|
|
_, output_callback, _ = await self._setup_app_with_callback_capture()
|
|
|
|
# Test the captured on_output callback with normal content
|
|
assert output_callback is not None
|
|
test_msg = StreamMessage(content="Hello, world!")
|
|
output_callback(test_msg)
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Hello, world!" in captured.out
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_output_with_empty_content(self, caplog):
|
|
"""Test on_output callback with empty content."""
|
|
with caplog.at_level(logging.DEBUG):
|
|
_, output_callback, _ = await self._setup_app_with_callback_capture()
|
|
|
|
# Test the captured on_output callback with empty content
|
|
assert output_callback is not None
|
|
test_msg = StreamMessage(content="")
|
|
output_callback(test_msg)
|
|
|
|
assert "Empty output received" in caplog.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_output_with_whitespace_only(self, caplog):
|
|
"""Test on_output callback with whitespace-only content."""
|
|
with caplog.at_level(logging.DEBUG):
|
|
_, output_callback, _ = await self._setup_app_with_callback_capture()
|
|
|
|
# Test the captured on_output callback with whitespace
|
|
assert output_callback is not None
|
|
test_msg = StreamMessage(content=" \n\t ")
|
|
output_callback(test_msg)
|
|
|
|
assert "Empty output received" in caplog.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_output_with_tool_commands(self, capsys):
|
|
"""Test on_output callback with tool commands."""
|
|
# Set up app but need to mock _process_tool_commands before session starts
|
|
app = ReactiveCleverAgentsApp(unsafe=True)
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Mock _process_tool_commands to verify it's called
|
|
app._process_tool_commands = Mock(return_value="Processed: tool command")
|
|
|
|
# Now use the rest of the setup
|
|
captured_callback = None
|
|
def capture_subscribe(observer):
|
|
nonlocal captured_callback
|
|
captured_callback = observer.on_next
|
|
|
|
app.stream_router = Mock()
|
|
app.stream_router.subscribe_to_output = capture_subscribe
|
|
app.stream_router.observables = {"__error__": Mock()}
|
|
app.stream_router.observables["__error__"].subscribe = Mock()
|
|
|
|
with patch('builtins.input', side_effect=['exit']):
|
|
await app.start_interactive_session()
|
|
|
|
# Test the captured on_output callback
|
|
assert captured_callback is not None
|
|
test_msg = StreamMessage(content="execute: some_tool")
|
|
captured_callback(test_msg)
|
|
|
|
# Verify _process_tool_commands was called
|
|
app._process_tool_commands.assert_called_once_with("execute: some_tool")
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Processed: tool command" in captured.out
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_error_callback(self, caplog):
|
|
"""Test on_error callback."""
|
|
with caplog.at_level(logging.ERROR):
|
|
_, _, error_callback = await self._setup_app_with_callback_capture()
|
|
|
|
# Test the captured on_error callback
|
|
assert error_callback is not None
|
|
error_msg = StreamMessage(content="Something went wrong!")
|
|
error_callback(error_msg)
|
|
|
|
assert "Something went wrong!" in caplog.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_on_error_with_exception_object(self, caplog):
|
|
"""Test on_error callback with exception object."""
|
|
with caplog.at_level(logging.ERROR):
|
|
app = ReactiveCleverAgentsApp(unsafe=True)
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Mock the stream router and capture the on_error callback
|
|
captured_error_callback = None
|
|
|
|
def capture_error_subscribe(observer):
|
|
nonlocal captured_error_callback
|
|
captured_error_callback = observer.on_next
|
|
|
|
app.stream_router = Mock()
|
|
app.stream_router.subscribe_to_output = Mock()
|
|
|
|
mock_error_observable = Mock()
|
|
mock_error_observable.subscribe = capture_error_subscribe
|
|
app.stream_router.observables = {"__error__": mock_error_observable}
|
|
|
|
# Mock input to exit immediately
|
|
with patch('builtins.input', side_effect=['exit']):
|
|
await app.start_interactive_session()
|
|
|
|
# Test the captured on_error callback with an exception
|
|
assert captured_error_callback is not None
|
|
|
|
error_msg = StreamMessage(content=Exception("Test exception"))
|
|
|
|
captured_error_callback(error_msg)
|
|
|
|
assert "Test exception" in caplog.text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_interactive_session_with_existing_config(self):
|
|
"""Test interactive session where self.config already exists."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
app.stream_router.subscribe_to_output = Mock()
|
|
app.stream_router.observables = {"__error__": Mock()}
|
|
app.stream_router.observables["__error__"].subscribe = Mock()
|
|
|
|
with patch('builtins.input', side_effect=['exit']):
|
|
await app.start_interactive_session()
|
|
|
|
assert app.config is not None
|
|
assert app.config.global_context == {}
|
|
|
|
|
|
class TestRegisterTemplatesExtended:
|
|
"""Extended test suite for _register_templates method."""
|
|
|
|
def _setup_app_with_templates(self, templates_config):
|
|
"""
|
|
Helper method to set up app with mock config and templates.
|
|
|
|
Args:
|
|
templates_config: Dictionary of templates configuration
|
|
|
|
Returns:
|
|
ReactiveCleverAgentsApp: Configured app instance
|
|
"""
|
|
app = ReactiveCleverAgentsApp()
|
|
mock_config = Mock()
|
|
mock_config.templates = templates_config
|
|
app.config = mock_config
|
|
return app
|
|
|
|
def test_register_templates_with_enhanced_registry(self):
|
|
"""Test registering templates with enhanced registry."""
|
|
templates_config = {
|
|
"agents": {
|
|
"template1": {
|
|
"_needs_preprocessing": True,
|
|
"_raw_template": "raw content",
|
|
}
|
|
}
|
|
}
|
|
app = self._setup_app_with_templates(templates_config)
|
|
app._register_templates()
|
|
|
|
# Should have initialized enhanced registry
|
|
assert app._use_enhanced_registry is True or app.template_registry is not None
|
|
|
|
def test_register_templates_with_regular_templates(self):
|
|
"""Test registering regular templates."""
|
|
templates_config = {
|
|
"agents": {
|
|
"template1": {"content": "regular template"}
|
|
}
|
|
}
|
|
app = self._setup_app_with_templates(templates_config)
|
|
app._register_templates()
|
|
|
|
assert app.template_registry is not None
|
|
|
|
|
|
class TestCreateAgentsExtended:
|
|
"""Extended test suite for _create_agents method."""
|
|
|
|
def test_create_agents_raises_without_factory(self):
|
|
"""Test that _create_agents raises without agent factory."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app.agent_factory = None
|
|
app.config = Mock()
|
|
|
|
with pytest.raises(Exception): # AgentCreationError or similar
|
|
app._create_agents()
|
|
|
|
def test_create_agents_registers_builtin_types(self):
|
|
"""Test that _create_agents registers built-in agent types."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_factory = Mock()
|
|
mock_factory.get_agent_types = Mock(return_value=[])
|
|
mock_factory.register_agent_type = Mock()
|
|
app.agent_factory = mock_factory
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {}
|
|
app.config = mock_config
|
|
|
|
app._create_agents()
|
|
|
|
# Should register exactly 2 built-in types: llm and tool
|
|
assert mock_factory.register_agent_type.call_count == 2
|
|
|
|
def test_create_agents_with_regular_agent(self):
|
|
"""Test creating regular (non-template) agent."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_factory = Mock()
|
|
mock_factory.get_agent_types = Mock(return_value=["llm", "tool"])
|
|
mock_factory.create_agent = Mock(return_value=Mock())
|
|
app.agent_factory = mock_factory
|
|
|
|
mock_agent_config = Mock()
|
|
mock_agent_config.type = "llm"
|
|
mock_agent_config.config = {"model": "gpt-4"}
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {"agent1": mock_agent_config}
|
|
app.config = mock_config
|
|
|
|
app.template_registry = Mock()
|
|
|
|
app._create_agents()
|
|
|
|
# Should have created agent
|
|
assert mock_factory.create_agent.called
|
|
|
|
|
|
class TestSetupPipelinesExtended:
|
|
"""Extended test suite for _setup_pipelines method."""
|
|
|
|
def test_setup_pipelines_basic(self):
|
|
"""Test basic pipelines setup."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_pipeline = Mock()
|
|
mock_pipeline.stages = []
|
|
|
|
# Pipelines is a dict
|
|
mock_config.pipelines = {"pipeline1": mock_pipeline}
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_pipelines()
|
|
|
|
|
|
class TestHandleGraphCommand:
|
|
"""Test suite for _handle_graph_command method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_with_list(self):
|
|
"""Test handling graph list command."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
app.config = mock_config
|
|
|
|
app.langgraph_bridge = Mock()
|
|
app.langgraph_bridge.list_graphs = Mock(return_value=["graph1", "graph2"])
|
|
|
|
# Should not raise
|
|
await app._handle_graph_command("list")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_invalid_format(self):
|
|
"""Test graph command with missing message prints usage."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
# Invalid format - only graph name, no message
|
|
await app._handle_graph_command("graph_name")
|
|
output = captured.getvalue()
|
|
|
|
# Should print usage message
|
|
assert "Usage" in output
|
|
assert "/graph" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_graph_not_found(self):
|
|
"""Test graph command with non-existent graph prints error."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
app.config = Mock()
|
|
app.config.routes = {} # No graphs defined
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
# Graph doesn't exist
|
|
await app._handle_graph_command("nonexistent test_message")
|
|
output = captured.getvalue()
|
|
|
|
# Should print graph not found error
|
|
assert "not found" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_not_graph_type(self):
|
|
"""Test graph command with route that's not a graph type."""
|
|
app = ReactiveCleverAgentsApp(config_files=None, unsafe=False)
|
|
app.config = Mock()
|
|
|
|
# Create a route that exists but is not a graph
|
|
mock_route = Mock()
|
|
mock_route.type = RouteType.STREAM # Not a graph!
|
|
app.config.routes = {"test_route": mock_route}
|
|
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured = io.StringIO()
|
|
|
|
try:
|
|
await app._handle_graph_command("test_route test_message")
|
|
output = captured.getvalue()
|
|
|
|
# Should indicate not a graph route
|
|
assert "not found" in output
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_with_graph_route_type(self):
|
|
"""Test handle graph command where route.type == RouteType.GRAPH."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_route = Mock()
|
|
mock_route.type = RouteType.GRAPH
|
|
mock_config.routes = {"my_graph": mock_route}
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
mock_graph = Mock()
|
|
mock_result = Mock()
|
|
mock_result.messages = [{"role": "assistant", "content": "Graph result"}]
|
|
mock_result.to_dict = Mock(return_value={"result": "data"})
|
|
mock_graph.execute = AsyncMock(return_value=mock_result)
|
|
|
|
app.langgraph_bridge.get_graph = Mock(return_value=mock_graph)
|
|
|
|
await app._handle_graph_command("my_graph test input")
|
|
|
|
assert mock_graph.execute.called
|
|
|
|
|
|
class TestLoadConfigurationWithFiles:
|
|
"""Test loading configuration with actual config files."""
|
|
|
|
def test_load_configuration_initializes_all_components(self):
|
|
"""Test that load_configuration initializes all components."""
|
|
temp_path = get_fixture_path("minimal_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
with patch.object(app, '_create_agents'):
|
|
with patch.object(app, '_setup_routes'):
|
|
with patch.object(app, '_setup_stream_operations'):
|
|
with patch.object(app, '_setup_pipelines'):
|
|
app.load_configuration([temp_path])
|
|
|
|
# Should have initialized template renderer
|
|
assert app.template_renderer is not None
|
|
assert app.agent_factory is not None
|
|
|
|
def test_initialization_with_config_files(self):
|
|
"""Test initialization with config files provided."""
|
|
temp_path = get_fixture_path("simple_config.yaml")
|
|
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._create_agents'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_routes'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_stream_operations'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_pipelines'):
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have loaded config
|
|
assert app.config is not None
|
|
|
|
|
|
class TestRunSingleShotIntegration:
|
|
"""Integration tests for run_single_shot with real Observer behavior."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_real_observer(self):
|
|
"""Test run_single_shot with actual Observer usage."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
# Store the observer that gets passed to subscribe_to_output
|
|
captured_observer = None
|
|
|
|
def capture_observer(observer):
|
|
nonlocal captured_observer
|
|
captured_observer = observer
|
|
# Immediately send a response
|
|
msg = StreamMessage(content="test response", metadata={})
|
|
observer.on_next(msg)
|
|
|
|
app.stream_router.subscribe_to_output = capture_observer
|
|
app.stream_router.send_message = Mock()
|
|
|
|
result = await app.run_single_shot("test prompt")
|
|
|
|
assert result == "test response"
|
|
assert captured_observer is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_message_having_no_content(self):
|
|
"""Test run_single_shot when message has no content attribute."""
|
|
app = ReactiveCleverAgentsApp()
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
def capture_and_respond(observer):
|
|
# Send message with no content
|
|
msg = Mock(spec=[]) # spec=[] means no attributes
|
|
observer.on_next(msg)
|
|
|
|
app.stream_router.subscribe_to_output = capture_and_respond
|
|
app.stream_router.send_message = Mock()
|
|
|
|
result = await app.run_single_shot("test")
|
|
|
|
assert result == ""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_observer_on_error(self):
|
|
"""Test run_single_shot when observer receives error."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {}
|
|
app.config = mock_config
|
|
|
|
def trigger_error(observer):
|
|
observer.on_error(Exception("Test error"))
|
|
|
|
app.stream_router.subscribe_to_output = trigger_error
|
|
app.stream_router.send_message = Mock()
|
|
|
|
with pytest.raises(Exception) as exc_info:
|
|
await app.run_single_shot("test")
|
|
|
|
assert "Test error" in str(exc_info.value)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_with_metadata(self):
|
|
"""Test run_single_shot passes metadata correctly."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.global_context = {"global_key": "global_value"}
|
|
app.config = mock_config
|
|
|
|
sent_metadata = None
|
|
|
|
def capture_message(stream, content, metadata):
|
|
nonlocal sent_metadata
|
|
sent_metadata = metadata
|
|
|
|
def send_response(observer):
|
|
msg = StreamMessage(content="response", metadata={})
|
|
observer.on_next(msg)
|
|
|
|
app.stream_router.send_message = capture_message
|
|
app.stream_router.subscribe_to_output = send_response
|
|
|
|
await app.run_single_shot("test", custom_key="custom_value")
|
|
|
|
assert sent_metadata is not None
|
|
assert sent_metadata["custom_key"] == "custom_value"
|
|
assert sent_metadata["context"]["global_key"] == "global_value"
|
|
assert "_unsafe_mode" in sent_metadata
|
|
|
|
|
|
class TestInitializationWithConfigFiles:
|
|
"""Test initialization with config files to cover lines 126-128."""
|
|
|
|
def test_initialization_loads_config_files(self):
|
|
"""Test that initialization with config_files loads them."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._create_agents'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_routes'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_stream_operations'):
|
|
with patch('cleveragents.core.application.ReactiveCleverAgentsApp._setup_pipelines'):
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Config should be loaded
|
|
assert app.config is not None
|
|
|
|
|
|
class TestRegisterTemplatesWithPreprocessing:
|
|
"""Test _register_templates with preprocessing markers."""
|
|
|
|
def test_register_templates_with_needs_preprocessing(self):
|
|
"""Test registering templates that need preprocessing."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.templates = {
|
|
"agents": {
|
|
"template1": {
|
|
"_needs_preprocessing": True,
|
|
"_raw_template": "{% if test %}value{% endif %}",
|
|
"type": "llm",
|
|
}
|
|
}
|
|
}
|
|
app.config = mock_config
|
|
|
|
# Should initialize enhanced registry
|
|
app._register_templates()
|
|
|
|
assert app._use_enhanced_registry is True
|
|
|
|
|
|
class TestCreateAgentsWithBuiltinTypes:
|
|
"""Test _create_agents with actual agent creation."""
|
|
|
|
def test_create_agents_register_llm_and_tool_types(self):
|
|
"""Test that LLM and Tool agent types are registered."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Use real agent factory with required args
|
|
mock_factory_config = {}
|
|
mock_template_renderer = Mock()
|
|
app.agent_factory = AgentFactory(mock_factory_config, mock_template_renderer)
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {}
|
|
app.config = mock_config
|
|
|
|
app._create_agents()
|
|
|
|
# Verify agent types were registered
|
|
types_after = set(app.agent_factory.get_agent_types())
|
|
|
|
# Should have registered both llm and tool types
|
|
assert "llm" in types_after
|
|
assert "tool" in types_after
|
|
|
|
|
|
class TestSetupRoutesWithTemplates:
|
|
"""Test _setup_routes with template configurations."""
|
|
|
|
def test_setup_routes_with_empty_routes_dict(self):
|
|
"""Test _setup_routes with empty routes dictionary."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.routes = {}
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_routes()
|
|
|
|
def test_setup_routes_with_template_config(self):
|
|
"""Test _setup_routes skips routes with missing template_config attribute."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_route = Mock(spec=['type']) # No template_config attribute
|
|
mock_route.type = RouteType.STREAM
|
|
mock_route.to_stream_config = Mock(return_value={
|
|
"name": "test_stream",
|
|
"type": "cold",
|
|
})
|
|
|
|
mock_config.routes = {"route1": mock_route}
|
|
app.config = mock_config
|
|
|
|
app.stream_router.create_stream = Mock()
|
|
|
|
app._setup_routes()
|
|
|
|
# Should have tried to create stream
|
|
assert app.stream_router.create_stream.called
|
|
|
|
|
|
class TestSetupStreamOperationsWithDict:
|
|
"""Test _setup_stream_operations with dictionary config."""
|
|
|
|
def test_setup_stream_operations_with_empty_dict(self):
|
|
"""Test _setup_stream_operations with empty dictionary."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.streams = {}
|
|
mock_config.merges = [] # Add merges attribute
|
|
mock_config.splits = [] # Add splits attribute
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_stream_operations()
|
|
|
|
|
|
class TestSetupPipelinesWithDict:
|
|
"""Test _setup_pipelines with dictionary config."""
|
|
|
|
def test_setup_pipelines_with_empty_dict(self):
|
|
"""Test _setup_pipelines with empty dictionary."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_config = Mock()
|
|
mock_config.pipelines = {}
|
|
app.config = mock_config
|
|
|
|
# Should not raise
|
|
app._setup_pipelines()
|
|
|
|
|
|
class TestProcessToolCommandsExtended:
|
|
"""Extended tests for _process_tool_commands."""
|
|
|
|
def test_process_tool_commands_with_multiple_commands(self):
|
|
"""Test processing multiple tool commands."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app._execute_single_tool = Mock(return_value="result")
|
|
|
|
content = (
|
|
"[TOOL_EXECUTE:tool1]\n{}\n[/TOOL_EXECUTE]\n"
|
|
"Some text\n"
|
|
"[TOOL_EXECUTE:tool2]\n{}\n[/TOOL_EXECUTE]"
|
|
)
|
|
|
|
app._process_tool_commands(content)
|
|
|
|
# Should have executed tools
|
|
assert app._execute_single_tool.call_count == 2
|
|
|
|
def test_process_tool_commands_with_json_params(self):
|
|
"""Test processing tool commands with JSON parameters."""
|
|
app = ReactiveCleverAgentsApp()
|
|
app._execute_single_tool = Mock(return_value="executed")
|
|
|
|
content = '[TOOL_EXECUTE:test]\n{"param": "value"}\n[/TOOL_EXECUTE]'
|
|
|
|
app._process_tool_commands(content)
|
|
|
|
# Should have parsed JSON params
|
|
assert app._execute_single_tool.called
|
|
|
|
|
|
class TestExecuteSingleToolExtended:
|
|
"""Extended tests for _execute_single_tool."""
|
|
|
|
def test_execute_single_tool_returns_not_found(self):
|
|
"""Test that unknown tool returns error message."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
result = app._execute_single_tool("nonexistent_tool", {})
|
|
|
|
assert "Error: No agent available to execute tool 'nonexistent_tool'" in result
|
|
|
|
def test_execute_single_tool_with_empty_params(self):
|
|
"""Test tool execution with empty parameters dict."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
result = app._execute_single_tool("test", {})
|
|
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestVisualizeNetworkExtended:
|
|
"""Extended tests for visualize_network."""
|
|
|
|
def test_visualize_network_with_agents(self):
|
|
"""Test network visualization with agents."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Set up agents dict (used by visualize_network)
|
|
app.agents = {"agent1": Mock(), "agent2": Mock()}
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {"agent1": Mock(), "agent2": Mock()}
|
|
mock_config.routes = {}
|
|
mock_config.merges = []
|
|
mock_config.splits = []
|
|
mock_config.pipelines = {}
|
|
app.config = mock_config
|
|
|
|
result = app.visualize_network("mermaid")
|
|
|
|
# Both agents should be in the visualization
|
|
assert "agent1" in result
|
|
assert "agent2" in result
|
|
assert "graph TD" in result
|
|
|
|
|
|
class TestHandleStreamCommandExtended:
|
|
"""Extended tests for _handle_stream_command."""
|
|
|
|
def test_handle_stream_command_prints_output(self, capsys):
|
|
"""Test that _handle_stream_command prints output."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# _handle_stream_command doesn't return a value, it prints
|
|
app._handle_stream_command("/help")
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Usage: /stream <name> <message>" in captured.out
|
|
|
|
def test_handle_stream_command_with_stream_name(self, capsys):
|
|
"""Test _handle_stream_command with stream name."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
app._handle_stream_command("test_stream test message")
|
|
|
|
captured = capsys.readouterr()
|
|
# Should print error for nonexistent stream
|
|
assert "Stream 'test_stream' not found" in captured.out
|
|
|
|
|
|
class TestConfigToDictExtended:
|
|
"""Extended tests for _config_to_dict."""
|
|
|
|
def test_config_to_dict_with_complex_config(self):
|
|
"""Test _config_to_dict with complex configuration."""
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
mock_agent = Mock()
|
|
mock_agent.type = "llm"
|
|
mock_agent.config = {"model": "gpt-4"}
|
|
|
|
mock_config = Mock()
|
|
mock_config.agents = {"agent1": mock_agent}
|
|
mock_config.routes = {}
|
|
mock_config.streams = {}
|
|
mock_config.pipelines = {}
|
|
mock_config.templates = {}
|
|
mock_config.global_context = {"key": "value"}
|
|
|
|
app.config = mock_config
|
|
|
|
result = app._config_to_dict()
|
|
|
|
assert isinstance(result, dict)
|
|
|
|
|
|
class TestRealIntegration:
|
|
"""Integration tests that exercise real code paths without heavy mocking."""
|
|
|
|
def test_init_with_config_files_real(self):
|
|
"""Test initialization with real config file loading."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
# Don't mock anything - let it execute
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have loaded config and created components
|
|
assert app.config is not None
|
|
assert app.agent_factory is not None
|
|
assert app.template_renderer is not None
|
|
|
|
def test_register_templates_real_execution(self):
|
|
"""Test _register_templates with real execution."""
|
|
temp_path = get_fixture_path("templates_config.yaml")
|
|
|
|
# Load config via app initialization
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have created registry during initialization
|
|
assert app.template_registry is not None
|
|
assert app.config is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_real_execution(self):
|
|
"""Test run_single_shot with more realistic setup."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Mock subscribe_to_output to immediately send a response
|
|
def mock_subscribe(observer):
|
|
# Immediately send a message
|
|
msg = StreamMessage(content="test response", metadata={})
|
|
observer.on_next(msg)
|
|
|
|
app.stream_router.subscribe_to_output = mock_subscribe
|
|
|
|
result = await app.run_single_shot("test input")
|
|
|
|
assert "test response" in result
|
|
|
|
|
|
class TestRegisterTemplatesWithNonDictTemplate:
|
|
"""Test _register_templates with valid templates."""
|
|
|
|
def test_register_templates_simple_dict(self):
|
|
"""Test registering templates with simple dict."""
|
|
temp_path = get_fixture_path("dict_template_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
# Should register templates
|
|
assert app.template_registry is not None
|
|
|
|
|
|
class TestRunSingleShotTimeoutAndErrors:
|
|
"""Test run_single_shot timeout and error handling."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_timeout_error(self):
|
|
"""Test run_single_shot raises timeout error."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Mock subscribe to never respond (causes timeout)
|
|
def no_response_subscribe(observer):
|
|
pass # Don't send anything
|
|
|
|
app.stream_router.subscribe_to_output = no_response_subscribe
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.run_single_shot("test")
|
|
|
|
assert "timed out" in str(exc_info.value).lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_single_shot_generic_exception(self):
|
|
"""Test run_single_shot wraps generic exceptions."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Make subscribe_to_output raise a generic exception
|
|
def raise_error(observer):
|
|
raise ValueError("Test error")
|
|
|
|
app.stream_router.subscribe_to_output = raise_error
|
|
|
|
with pytest.raises(CleverAgentsException) as exc_info:
|
|
await app.run_single_shot("test")
|
|
|
|
# Should be wrapped in CleverAgentsException
|
|
assert "Failed to run" in str(exc_info.value) or "Test error" in str(exc_info.value)
|
|
|
|
|
|
class TestCreateAgentsWithTemplates:
|
|
"""Test _create_agents with template configurations."""
|
|
|
|
def test_create_agents_with_agent_config(self):
|
|
"""Test creating agents from configuration."""
|
|
temp_path = get_fixture_path("tool_agent_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have created agent
|
|
assert "test_tool" in app.agents
|
|
|
|
def test_create_agents_with_composite_agent(self):
|
|
"""Test creating composite agents."""
|
|
temp_path = get_fixture_path("composite_agent_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have created composite agent
|
|
assert "composite_agent" in app.agents
|
|
|
|
|
|
class TestSetupRoutesWithConfig:
|
|
"""Test _setup_routes with actual route configurations."""
|
|
|
|
def test_setup_routes_from_config(self):
|
|
"""Test setting up routes from configuration."""
|
|
temp_path = get_fixture_path("routes_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have processed routes without error
|
|
assert app.config is not None
|
|
|
|
|
|
class TestSetupStreamOperationsWithConfig:
|
|
"""Test _setup_stream_operations with configurations."""
|
|
|
|
def test_setup_streams_from_config(self):
|
|
"""Test setting up streams from configuration."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have processed streams without error
|
|
assert app.config is not None
|
|
|
|
|
|
class TestSetupPipelinesWithConfig:
|
|
"""Test _setup_pipelines with configurations."""
|
|
|
|
def test_setup_pipelines_from_config(self):
|
|
"""Test setting up pipelines from configuration."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Should have processed pipelines without error
|
|
assert app.config is not None
|
|
|
|
|
|
class TestHandleGraphCommandExtended:
|
|
"""Extended tests for _handle_graph_command."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_with_list_command(self):
|
|
"""Test _handle_graph_command with list command."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Mock langgraph_bridge
|
|
app.langgraph_bridge = Mock()
|
|
app.langgraph_bridge.list_graphs = Mock(return_value=["graph1", "graph2"])
|
|
|
|
# Should not raise
|
|
await app._handle_graph_command("list")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_with_visualize(self):
|
|
"""Test _handle_graph_command with visualize command."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Mock langgraph_bridge
|
|
app.langgraph_bridge = Mock()
|
|
mock_graph = Mock()
|
|
mock_graph.visualize = Mock(return_value="graph_visualization")
|
|
app.langgraph_bridge.get_graph = Mock(return_value=mock_graph)
|
|
|
|
# Should not raise
|
|
await app._handle_graph_command("visualize test_graph")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_graph_command_with_execute(self):
|
|
"""Test _handle_graph_command with execute command."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
# Mock langgraph_bridge
|
|
app.langgraph_bridge = Mock()
|
|
mock_graph = Mock()
|
|
mock_graph.execute = AsyncMock(return_value={"result": "test"})
|
|
app.langgraph_bridge.get_graph = Mock(return_value=mock_graph)
|
|
|
|
# Should execute without error
|
|
await app._handle_graph_command("execute test_graph test input")
|
|
|
|
|
|
class TestVisualizeNetworkIntegration:
|
|
"""Integration tests for visualize_network with real config files."""
|
|
|
|
def test_visualize_network_with_routes(self):
|
|
"""Test visualizing network with routes."""
|
|
temp_path = get_fixture_path("routes_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
result = app.visualize_network("mermaid")
|
|
|
|
# Should generate visualization
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_visualize_network_text_format(self):
|
|
"""Test visualizing network in text format."""
|
|
temp_path = get_fixture_path("empty_config.yaml")
|
|
|
|
app = ReactiveCleverAgentsApp(config_files=[temp_path])
|
|
|
|
result = app.visualize_network("text")
|
|
|
|
# Should generate text visualization
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestLoadConfigurationEdgeCases:
|
|
"""Test load_configuration edge cases."""
|
|
|
|
def test_load_configuration_with_missing_cleveragents_section(self):
|
|
"""Test loading configuration without cleveragents section."""
|
|
config_content = """
|
|
some_other_section:
|
|
value: "test"
|
|
"""
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', prefix='cleveragent_', suffix='.yaml', delete=False) as f:
|
|
f.write(config_content)
|
|
temp_path = Path(f.name)
|
|
|
|
try:
|
|
app = ReactiveCleverAgentsApp()
|
|
|
|
# Will load but may have issues
|
|
try:
|
|
app.load_configuration([temp_path])
|
|
except Exception:
|
|
pass # Expected - config may be invalid
|
|
finally:
|
|
temp_path.unlink()
|
|
|
|
|
|
class TestSanitizeJsonStringExtended:
|
|
"""Extended tests for _sanitize_json_string."""
|
|
|
|
def test_sanitize_json_with_backslashes(self):
|
|
"""Test sanitizing JSON with backslashes."""
|
|
json_str = '{"path": "C:\\\\Users\\\\test"}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(json_str)
|
|
|
|
# Should handle backslashes correctly and be valid JSON
|
|
parsed = json.loads(result)
|
|
assert "path" in parsed
|
|
assert "Users" in parsed["path"]
|
|
assert "test" in parsed["path"]
|
|
|
|
def test_sanitize_json_with_quotes(self):
|
|
"""Test sanitizing JSON with nested quotes."""
|
|
json_str = '{"message": "He said \\"hello\\""}'
|
|
result = ReactiveCleverAgentsApp._sanitize_json_string(json_str)
|
|
|
|
# Should handle quotes correctly and be valid JSON
|
|
parsed = json.loads(result)
|
|
assert "message" in parsed
|
|
assert "hello" in parsed["message"]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|
|
|