Files
temp/tests/unit/core/test_application.py

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