f281fa3b24
CI / lint (pull_request) Successful in 35s
CI / typecheck (pull_request) Successful in 50s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 32s
CI / unit_tests (pull_request) Successful in 3m32s
CI / integration_tests (pull_request) Successful in 59s
CI / build (pull_request) Successful in 34s
CI / lint (push) Successful in 33s
CI / build (push) Successful in 37s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 48s
CI / security (push) Successful in 1m4s
CI / integration_tests (push) Successful in 1m18s
CI / coverage (pull_request) Successful in 3m34s
CI / status-check (pull_request) Successful in 3s
CI / unit_tests (push) Successful in 3m45s
CI / coverage (push) Successful in 3m34s
CI / status-check (push) Successful in 3s
Implements Wave 3 of the cleveractors-core integration (ADR-2026, ADR-2028). ## AgentFactory - Added optional credentials: dict[str, dict[str, str]] | None parameter to __init__. Stored as self.credentials. - When creating LLMAgent instances, the full credentials dict is forwarded via a new credentials constructor kwarg, threading per-request API keys without touching the stored actor config_dict. - Added explicit type annotation ReactiveAgentFactory: type[AgentFactory]. - _create_agent_instance raises ConfigurationError when credentials is not None but does not contain an entry for the requested provider (AC4 compliance). ## LLMAgent - Added optional credentials: dict[str, str] | None parameter to __init__. Stored as self._credentials; config_dict is never modified. - LangChain client construction deferred to first access (lazy init) via a chat_model property backed by _ensure_chat_model(). The property setter allows test-injection of mock models without going through the lazy-init path. - _create_chat_model() dispatches to credential-injection / standalone paths. - Extracted to llm_client.py and llm_providers.py for modularity. - Defined shared module-level constants DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS to eliminate duplicated magic numbers across llm.py, runtime.py, and factory.py. - Added public credentials property exposing self._credentials. - Added type narrowing guard in chat_model property to satisfy static analysis. - Added temperature_override type validation in process_message. - Guarded cleanup() null-assignment with _chat_model_lock (TOCTOU fix). ## SSRF Prevention (ADR-2028) - _validate_base_url(): https-only, userinfo rejection, localhost rejection, raw IP literal rejection, numeric-hostname (dotted-hex/octal/compact) rejection, percent-decode loop with iteration cap (10), trailing-dot FQDN strip. - DNS resolution intentionally omitted (blocking call in async path). - Canonical URL reconstruction uses lowercased scheme. ## Credential Leak Prevention - All logger.exception(...) replaced with logger.debug(..., exc_info=True). - Error messages use type(e).__name__ instead of str(e). - process_message exception chains broken with from None to prevent LangChain authentication errors from leaking via __cause__. ## Runtime fixes - Executor._execute_llm: shallow copy for setdefault mutations. - Executor._execute_graph: restructured try/finally for resource cleanup. - Uses DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS from llm.py. - Defensive copy of credentials dict in Executor.__init__ to prevent caller mutation after construction. ## Test improvements - Added BDD scenarios for llm_imports.py ImportError fallback branches using sys.modules manipulation with a custom import hook, exercised via subprocess. - Added BDD scenario for AgentFactory.create_agents_from_config() credential threading to all created agents. - Removed tautological dataclass test scenarios (NodeUsage, ActorResult) from runtime_coverage.feature. - Fixed misleading scenario title in runtime_coverage.feature (exception chain suppressed, not chained) and updated step to assert __cause__ is None. - Added CHANGELOG entry for ReactiveAgentFactory backward-compatibility alias. - Removed duplicate log emission in _check_known_provider_domain. ISSUES CLOSED: #12
984 lines
33 KiB
Python
984 lines
33 KiB
Python
"""
|
|
Step definitions for Agent Module Integration BDD tests.
|
|
"""
|
|
|
|
from typing import Any
|
|
from unittest.mock import Mock, patch
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveractors.core.exceptions import AgentCreationError, ConfigurationError
|
|
|
|
|
|
@given("the agent system is initialized")
|
|
def step_agent_system_initialized(context):
|
|
"""Initialize the agent system."""
|
|
context.agents = []
|
|
context.agent_modules = {}
|
|
context.error = None
|
|
context.agent_results = {}
|
|
|
|
|
|
@given("I have agent test configurations")
|
|
def step_agent_test_configurations(context):
|
|
"""Set up agent test configurations."""
|
|
context.agent_configs = {
|
|
"llm_agent": {
|
|
"name": "test_llm",
|
|
"type": "llm",
|
|
"config": {"model": "gpt-3.5-turbo"},
|
|
},
|
|
"tool_agent": {
|
|
"name": "test_tool",
|
|
"type": "tool",
|
|
"config": {"tools": ["math"]},
|
|
},
|
|
"chain_agent": {
|
|
"name": "test_chain",
|
|
"type": "chain",
|
|
"config": {"agents": ["agent1", "agent2"]},
|
|
},
|
|
}
|
|
|
|
|
|
@when("I import the agent module")
|
|
def step_import_agent_module(context):
|
|
"""Import the agent module."""
|
|
try:
|
|
import cleveractors.agent as agent_module
|
|
|
|
context.agent_module = agent_module
|
|
|
|
# Access module attributes for coverage
|
|
attrs = dir(agent_module)
|
|
context.agent_attrs = attrs
|
|
|
|
# Try to access any classes or functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(agent_module, attr_name, None)
|
|
if attr:
|
|
context.agent_attr = attr
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("the agent module should be accessible")
|
|
def step_agent_module_accessible(context):
|
|
"""Verify agent module is accessible."""
|
|
assert context.agent_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("agent creation should work")
|
|
def step_agent_creation_works(context):
|
|
"""Verify agent creation works."""
|
|
assert hasattr(context, "agent_module")
|
|
|
|
|
|
@when("I import the decorators module")
|
|
def step_import_decorators_module(context):
|
|
"""Import the decorators module."""
|
|
try:
|
|
import cleveractors.agents.decorators as decorators_module
|
|
|
|
context.decorators_module = decorators_module
|
|
|
|
# Access module for coverage
|
|
attrs = dir(decorators_module)
|
|
context.decorators_attrs = attrs
|
|
|
|
# Try to access decorators
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(decorators_module, attr_name, None)
|
|
if callable(attr):
|
|
try:
|
|
# Get function info for coverage
|
|
context.decorator_func = attr
|
|
if hasattr(attr, "__doc__"):
|
|
context.decorator_doc = attr.__doc__
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("decorators should be available")
|
|
def step_decorators_available(context):
|
|
"""Verify decorators are available."""
|
|
assert context.decorators_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("decorator functions should work")
|
|
def step_decorator_functions_work(context):
|
|
"""Verify decorator functions work."""
|
|
assert hasattr(context, "decorators_module")
|
|
|
|
|
|
@when("I import the states module")
|
|
def step_import_states_module(context):
|
|
"""Import the states module."""
|
|
try:
|
|
import cleveractors.agents.states as states_module
|
|
|
|
context.states_module = states_module
|
|
|
|
# Access module for coverage
|
|
attrs = dir(states_module)
|
|
context.states_attrs = attrs
|
|
|
|
# Try to access state classes/functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(states_module, attr_name, None)
|
|
if attr:
|
|
context.states_attr = attr
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("state management should be available")
|
|
def step_state_management_available(context):
|
|
"""Verify state management is available."""
|
|
assert context.states_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("state transitions should work")
|
|
def step_state_transitions_work(context):
|
|
"""Verify state transitions work."""
|
|
assert hasattr(context, "states_module")
|
|
|
|
|
|
@given("I have agent factory configurations")
|
|
def step_agent_factory_configurations(context):
|
|
"""Set up agent factory configurations."""
|
|
context.factory_configs = {
|
|
"agents": [
|
|
{"type": "llm", "name": "factory_llm"},
|
|
{"type": "tool", "name": "factory_tool"},
|
|
{"type": "chain", "name": "factory_chain"},
|
|
]
|
|
}
|
|
|
|
|
|
@when("I use the agent factory")
|
|
def step_use_agent_factory(context):
|
|
"""Use the agent factory."""
|
|
try:
|
|
import cleveractors.agents.factory as factory_module
|
|
|
|
context.factory_module = factory_module
|
|
|
|
# Access factory for coverage
|
|
attrs = dir(factory_module)
|
|
context.factory_attrs = attrs
|
|
|
|
# Try to access factory classes/functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(factory_module, attr_name, None)
|
|
if callable(attr):
|
|
try:
|
|
# Get class/function info for coverage
|
|
context.factory_callable = attr
|
|
if hasattr(attr, "__init__"):
|
|
# It's a class
|
|
context.factory_class = attr
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("agents should be created correctly")
|
|
def step_agents_created_correctly(context):
|
|
"""Verify agents are created correctly."""
|
|
assert context.factory_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("factory patterns should work")
|
|
def step_factory_patterns_work(context):
|
|
"""Verify factory patterns work."""
|
|
assert hasattr(context, "factory_module")
|
|
|
|
|
|
@given("I have chain agent configurations")
|
|
def step_chain_agent_configurations(context):
|
|
"""Set up chain agent configurations."""
|
|
context.chain_configs = {
|
|
"chain_name": "test_chain",
|
|
"agents": ["agent1", "agent2", "agent3"],
|
|
"flow": "sequential",
|
|
}
|
|
|
|
|
|
@when("I create chain agents")
|
|
def step_create_chain_agents(context):
|
|
"""Create chain agents."""
|
|
try:
|
|
import cleveractors.agents.chain as chain_module
|
|
|
|
context.chain_module = chain_module
|
|
|
|
# Access chain module for coverage
|
|
attrs = dir(chain_module)
|
|
context.chain_attrs = attrs
|
|
|
|
# Try to access chain classes
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(chain_module, attr_name, None)
|
|
if attr and hasattr(attr, "__init__"):
|
|
try:
|
|
# It's a class - try to get its methods
|
|
methods = [
|
|
method for method in dir(attr) if not method.startswith("_")
|
|
]
|
|
context.chain_methods = methods
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("agents should be chained correctly")
|
|
def step_agents_chained_correctly(context):
|
|
"""Verify agents are chained correctly."""
|
|
assert context.chain_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("message passing should work")
|
|
def step_message_passing_works(context):
|
|
"""Verify message passing works."""
|
|
assert hasattr(context, "chain_module")
|
|
|
|
|
|
@given("I have composite agent configurations")
|
|
def step_composite_agent_configurations(context):
|
|
"""Set up composite agent configurations."""
|
|
context.composite_configs = {
|
|
"composite_name": "test_composite",
|
|
"sub_agents": ["llm_agent", "tool_agent"],
|
|
"coordination": "parallel",
|
|
}
|
|
|
|
|
|
@when("I create composite agents")
|
|
def step_create_composite_agents(context):
|
|
"""Create composite agents."""
|
|
try:
|
|
import cleveractors.agents.composite as composite_module
|
|
|
|
context.composite_module = composite_module
|
|
|
|
# Access composite module for coverage
|
|
attrs = dir(composite_module)
|
|
context.composite_attrs = attrs
|
|
|
|
# Try to access composite classes
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(composite_module, attr_name, None)
|
|
if attr:
|
|
context.composite_attr = attr
|
|
# Try to get class properties
|
|
if hasattr(attr, "__dict__"):
|
|
context.composite_properties = list(attr.__dict__.keys())
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("agent composition should work")
|
|
def step_agent_composition_works(context):
|
|
"""Verify agent composition works."""
|
|
assert context.composite_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("parallel processing should work")
|
|
def step_parallel_processing_works(context):
|
|
"""Verify parallel processing works."""
|
|
assert hasattr(context, "composite_module")
|
|
|
|
|
|
@given("I have LLM agent configurations")
|
|
def step_llm_agent_configurations(context):
|
|
"""Set up LLM agent configurations."""
|
|
context.llm_configs = {
|
|
"model": "gpt-3.5-turbo",
|
|
"temperature": 0.7,
|
|
"max_tokens": 1000,
|
|
"api_key": "test-key",
|
|
}
|
|
|
|
|
|
@when("I create LLM agents")
|
|
def step_create_llm_agents(context):
|
|
"""Create LLM agents."""
|
|
try:
|
|
import cleveractors.agents.llm as llm_module
|
|
|
|
context.llm_module = llm_module
|
|
|
|
# Access LLM module for coverage
|
|
attrs = dir(llm_module)
|
|
context.llm_attrs = attrs
|
|
|
|
# Try to access LLM classes/functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(llm_module, attr_name, None)
|
|
if attr:
|
|
context.llm_attr = attr
|
|
# Try to access class methods
|
|
if hasattr(attr, "__class__"):
|
|
context.llm_class_info = str(attr.__class__)
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("LLM integration should work")
|
|
def step_llm_integration_works(context):
|
|
"""Verify LLM integration works."""
|
|
assert context.llm_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("model communication should work")
|
|
def step_model_communication_works(context):
|
|
"""Verify model communication works."""
|
|
assert hasattr(context, "llm_module")
|
|
|
|
|
|
@given("I have tool agent configurations")
|
|
def step_tool_agent_configurations(context):
|
|
"""Set up tool agent configurations."""
|
|
context.tool_configs = {
|
|
"tools": [
|
|
{"name": "math", "function": "calculate"},
|
|
{"name": "web_search", "function": "search"},
|
|
{"name": "file_reader", "function": "read_file"},
|
|
],
|
|
"execution_mode": "sequential",
|
|
}
|
|
|
|
|
|
@when("I create tool agents")
|
|
def step_create_tool_agents(context):
|
|
"""Create tool agents."""
|
|
try:
|
|
import cleveractors.agents.tool as tool_module
|
|
|
|
context.tool_module = tool_module
|
|
|
|
# Access tool module for coverage
|
|
attrs = dir(tool_module)
|
|
context.tool_attrs = attrs
|
|
|
|
# Try to access tool classes/functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(tool_module, attr_name, None)
|
|
if callable(attr):
|
|
try:
|
|
# Get callable info for coverage
|
|
context.tool_callable = attr
|
|
if hasattr(attr, "__name__"):
|
|
context.tool_name = attr.__name__
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("tool execution should work")
|
|
def step_tool_execution_works(context):
|
|
"""Verify tool execution works."""
|
|
assert context.tool_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("tool chaining should work")
|
|
def step_tool_chaining_works(context):
|
|
"""Verify tool chaining works."""
|
|
assert hasattr(context, "tool_module")
|
|
|
|
|
|
@when("I import the network module")
|
|
def step_import_network_module(context):
|
|
"""Import the network module."""
|
|
try:
|
|
import cleveractors.network as network_module
|
|
|
|
context.network_module = network_module
|
|
|
|
# Access network module for coverage
|
|
attrs = dir(network_module)
|
|
context.network_attrs = attrs
|
|
|
|
# Try to access network classes/functions
|
|
for attr_name in attrs:
|
|
if not attr_name.startswith("_"):
|
|
attr = getattr(network_module, attr_name, None)
|
|
if attr:
|
|
context.network_attr = attr
|
|
# Try to get attribute type info
|
|
context.network_attr_type = str(type(attr))
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("network functionality should be available")
|
|
def step_network_functionality_available(context):
|
|
"""Verify network functionality is available."""
|
|
assert context.network_module is not None
|
|
assert context.error is None
|
|
|
|
|
|
@then("agent communication should work")
|
|
def step_agent_communication_works(context):
|
|
"""Verify agent communication works."""
|
|
assert hasattr(context, "network_module")
|
|
|
|
|
|
@given("I have comprehensive agent factory configurations")
|
|
def step_comprehensive_agent_factory_configurations(context):
|
|
"""Set up comprehensive agent factory configurations."""
|
|
context.factory_configs = {
|
|
"agents": {
|
|
"composite_with_invalid_components": {
|
|
"type": "composite",
|
|
"config": {
|
|
"components": {
|
|
"agents": {
|
|
"invalid_agent1": "not_a_dict",
|
|
"invalid_agent2": {"no_type": "present"},
|
|
"valid_agent": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-3.5-turbo"},
|
|
},
|
|
}
|
|
}
|
|
},
|
|
},
|
|
"composite_with_legacy": {
|
|
"type": "composite",
|
|
"config": {"agents": ["missing_agent1", "missing_agent2"]},
|
|
},
|
|
"agent_without_config": {"type": "llm"},
|
|
}
|
|
}
|
|
|
|
|
|
@when("I test agent factory edge cases")
|
|
def step_test_agent_factory_edge_cases(context):
|
|
"""Test agent factory edge cases."""
|
|
try:
|
|
from unittest.mock import Mock, patch
|
|
|
|
import cleveractors.agents.factory as factory_module
|
|
from cleveractors.templates.renderer import TemplateRenderer
|
|
|
|
context.factory_module = factory_module
|
|
|
|
# Create mock template renderer
|
|
mock_template_renderer = Mock(spec=TemplateRenderer)
|
|
|
|
# Create factory
|
|
factory = factory_module.AgentFactory(
|
|
config=context.factory_configs, template_renderer=mock_template_renderer
|
|
)
|
|
|
|
context.factory = factory
|
|
context.test_results = {}
|
|
context.test_errors: dict[str, Exception | None] = {}
|
|
|
|
# Test composite agent with invalid components
|
|
with patch("cleveractors.agents.composite.CompositeAgent") as mock_composite:
|
|
mock_composite_instance = Mock()
|
|
mock_composite.return_value = mock_composite_instance
|
|
|
|
try:
|
|
composite_agent = factory.create_agent(
|
|
"composite_with_invalid_components"
|
|
)
|
|
context.test_errors["composite_invalid"] = None
|
|
except Exception as e:
|
|
context.test_errors["composite_invalid"] = e
|
|
context.test_results["composite_invalid"] = mock_composite_instance
|
|
|
|
# Test legacy composite with missing agents (empty cache).
|
|
# The factory now raises AgentCreationError for unknown agents
|
|
# referenced in a legacy composite, instead of silently skipping.
|
|
factory.agents = {} # Ensure cache is empty
|
|
try:
|
|
with patch(
|
|
"cleveractors.agents.composite.CompositeAgent"
|
|
) as mock_composite:
|
|
mock_composite_instance = Mock()
|
|
mock_composite.return_value = mock_composite_instance
|
|
|
|
composite_agent = factory.create_agent("composite_with_legacy")
|
|
context.test_results["legacy_missing"] = mock_composite_instance
|
|
context.test_errors["legacy_missing"] = None
|
|
except Exception as e:
|
|
context.test_errors["legacy_missing"] = e
|
|
|
|
# Test validation with agent configuration without config section
|
|
factory.validate_configuration()
|
|
context.test_results["validation_no_config"] = True
|
|
|
|
# Test get_agent_metadata for agent without config section
|
|
metadata = factory.get_agent_metadata("agent_without_config")
|
|
context.test_results["metadata_no_config"] = metadata
|
|
|
|
except Exception as e:
|
|
context.error = e
|
|
|
|
|
|
@then("factory should handle composite agent invalid components")
|
|
def step_factory_handle_invalid_components(context):
|
|
"""Verify factory handles invalid components correctly."""
|
|
# Composite with invalid components should not raise (invalid agents
|
|
# are silently skipped by the components-processing path, which is
|
|
# by design for backward compatibility with partial configs).
|
|
error = context.test_errors.get("composite_invalid")
|
|
assert error is None, (
|
|
f"Expected no error for composite_with_invalid_components, but got: {error!r}"
|
|
)
|
|
assert "composite_invalid" in context.test_results
|
|
# Should only have been called for the valid agent
|
|
composite_instance = context.test_results["composite_invalid"]
|
|
add_agent_calls = composite_instance.add_agent.call_args_list
|
|
assert len(add_agent_calls) == 1 # Only valid_agent should be added
|
|
|
|
|
|
@then("factory should handle legacy agent missing from cache")
|
|
def step_factory_handle_missing_legacy_agents(context):
|
|
"""Verify factory raises AgentCreationError for unknown legacy agents.
|
|
|
|
Previously the legacy composite path silently skipped agents that were
|
|
neither in the cache nor in the agent config. This was a bug — a
|
|
composite referencing a non-existent agent should fail fast with a
|
|
clear error.
|
|
"""
|
|
from cleveractors.core.exceptions import AgentCreationError
|
|
|
|
error = context.test_errors.get("legacy_missing")
|
|
assert error is not None, (
|
|
"Expected AgentCreationError for legacy composite with unknown "
|
|
"agent, but no error was raised"
|
|
)
|
|
assert isinstance(error, AgentCreationError), (
|
|
f"Expected AgentCreationError, got {type(error).__name__}: {error}"
|
|
)
|
|
assert "Unknown agent" in str(error), (
|
|
f"Error message should mention unknown agent, got: {error!r}"
|
|
)
|
|
|
|
|
|
@then("factory should handle agent configuration without config section")
|
|
def step_factory_handle_no_config_section(context):
|
|
"""Verify factory handles agents without config section correctly."""
|
|
assert context.error is None
|
|
assert context.test_results["validation_no_config"] is True
|
|
assert context.test_results["metadata_no_config"] is not None
|
|
metadata = context.test_results["metadata_no_config"]
|
|
assert metadata["name"] == "agent_without_config"
|
|
assert metadata["type"] == "llm"
|
|
|
|
|
|
# Additional comprehensive factory tests
|
|
@given("I have detailed factory configurations for comprehensive testing")
|
|
def step_detailed_factory_configurations(context):
|
|
"""Set up detailed factory configurations for comprehensive testing."""
|
|
|
|
from cleveractors.templates.renderer import TemplateRenderer
|
|
|
|
context.mock_template_renderer = Mock(spec=TemplateRenderer)
|
|
context.comprehensive_config = {
|
|
"agents": {
|
|
"test_cache_agent": {"type": "llm", "config": {"model": "gpt-3.5-turbo"}},
|
|
"missing_config_test": {"type": "llm"},
|
|
"unknown_type_test": {"type": "unknown_agent_type", "config": {}},
|
|
"tool_metadata_test": {
|
|
"type": "tool",
|
|
"config": {
|
|
"tools": ["math", "search"],
|
|
"allow_shell": True,
|
|
"safe_mode": False,
|
|
},
|
|
},
|
|
"invalid_config_dict": "not_a_dict",
|
|
"no_type_agent": {"config": {"some": "value"}},
|
|
"invalid_config_section": {"type": "llm", "config": "not_a_dict"},
|
|
}
|
|
}
|
|
|
|
|
|
@when("I perform comprehensive factory testing scenarios")
|
|
def step_perform_comprehensive_factory_testing(context):
|
|
"""Perform comprehensive factory testing scenarios."""
|
|
|
|
import cleveractors.agents.factory as factory_module
|
|
|
|
context.factory = factory_module.AgentFactory(
|
|
config=context.comprehensive_config,
|
|
template_renderer=context.mock_template_renderer,
|
|
stream_router=Mock(),
|
|
langgraph_bridge=Mock(),
|
|
)
|
|
|
|
context.test_results = {}
|
|
|
|
# Test 1: Agent caching
|
|
with patch("cleveractors.agents.llm.LLMAgent") as mock_llm:
|
|
mock_instance = Mock()
|
|
mock_llm.return_value = mock_instance
|
|
|
|
agent1 = context.factory.create_agent("test_cache_agent")
|
|
agent2 = context.factory.create_agent("test_cache_agent")
|
|
context.test_results["cache_test"] = agent1 is agent2
|
|
|
|
# Test 2: Agent without config section
|
|
try:
|
|
metadata = context.factory.get_agent_metadata("missing_config_test")
|
|
context.test_results["no_config_metadata"] = metadata
|
|
except Exception as e:
|
|
context.test_results["no_config_metadata_error"] = e
|
|
|
|
# Test 3: Unknown agent type
|
|
try:
|
|
context.factory.create_agent("unknown_type_test")
|
|
context.test_results["unknown_type_error"] = None
|
|
except Exception as e:
|
|
context.test_results["unknown_type_error"] = e
|
|
|
|
# Test 4: Tool agent metadata
|
|
try:
|
|
metadata = context.factory.get_agent_metadata("tool_metadata_test")
|
|
context.test_results["tool_metadata"] = metadata
|
|
except Exception as e:
|
|
context.test_results["tool_metadata_error"] = e
|
|
|
|
# Test 5: Agent types registration
|
|
from cleveractors.agents.base import Agent
|
|
|
|
class TestAgent(Agent):
|
|
def __init__(self, name, config, template_renderer):
|
|
super().__init__(name, config, template_renderer)
|
|
|
|
def process(self, input_data):
|
|
return input_data
|
|
|
|
try:
|
|
context.factory.register_agent_type("test_agent", TestAgent)
|
|
context.test_results["register_valid"] = True
|
|
except Exception as e:
|
|
context.test_results["register_valid_error"] = e
|
|
|
|
# Test 6: Invalid agent class registration
|
|
class InvalidAgent:
|
|
pass
|
|
|
|
try:
|
|
context.factory.register_agent_type("invalid_agent", InvalidAgent)
|
|
context.test_results["register_invalid"] = False
|
|
except Exception as e:
|
|
context.test_results["register_invalid_error"] = e
|
|
|
|
# Test missing agent creation
|
|
try:
|
|
missing_agent = context.factory.create_agent("completely_missing_agent")
|
|
context.test_results["missing_agent_error"] = None
|
|
except Exception as e:
|
|
context.test_results["missing_agent_error"] = e
|
|
|
|
# Test legacy composite with non-existent agent — should now raise
|
|
# AgentCreationError (previously silently skipped, which was a bug).
|
|
with patch("cleveractors.agents.composite.CompositeAgent") as mock_composite:
|
|
mock_composite_instance = Mock()
|
|
mock_composite.return_value = mock_composite_instance
|
|
|
|
# Create and cache an agent first
|
|
with patch("cleveractors.agents.llm.LLMAgent") as mock_llm:
|
|
mock_llm_instance = Mock()
|
|
mock_llm.return_value = mock_llm_instance
|
|
context.factory.agents["legacy_cached_agent"] = mock_llm_instance
|
|
|
|
# Create factory with legacy composite config referencing a
|
|
# non-existent agent (not in cache, not in agents config).
|
|
legacy_config: dict[str, Any] = {
|
|
"agents": {
|
|
"legacy_composite": {
|
|
"type": "composite",
|
|
"config": {"agents": ["legacy_cached_agent", "non_cached_agent"]},
|
|
}
|
|
}
|
|
}
|
|
|
|
legacy_factory = factory_module.AgentFactory(
|
|
config=legacy_config, template_renderer=context.mock_template_renderer
|
|
)
|
|
legacy_factory.agents["legacy_cached_agent"] = mock_llm_instance # Put in cache
|
|
|
|
try:
|
|
legacy_composite = legacy_factory.create_agent("legacy_composite")
|
|
context.test_results["legacy_composite_test"] = mock_composite_instance
|
|
context.test_results["legacy_missing_agent_skipped"] = True
|
|
except AgentCreationError as e:
|
|
context.test_results["legacy_missing_agent_error"] = e
|
|
|
|
# Test agent creation failure with constructor exception
|
|
creation_failure_factory = factory_module.AgentFactory(
|
|
config={
|
|
"agents": {
|
|
"failure_test_agent": {
|
|
"type": "llm",
|
|
"config": {"model": "gpt-3.5-turbo"},
|
|
}
|
|
}
|
|
},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
|
|
# Mock the LLMAgent constructor to raise an exception during initialization
|
|
original_llm_agent = None
|
|
try:
|
|
import cleveractors.agents.llm
|
|
|
|
original_llm_agent = cleveractors.agents.llm.LLMAgent
|
|
|
|
class FailingLLMAgent:
|
|
def __init__(self, *args, **kwargs):
|
|
raise ValueError("Mock creation failure")
|
|
|
|
cleveractors.agents.llm.LLMAgent = FailingLLMAgent
|
|
failed_agent = creation_failure_factory.create_agent("failure_test_agent")
|
|
context.test_results["creation_failure_error"] = None
|
|
except Exception as e:
|
|
context.test_results["creation_failure_error"] = e
|
|
finally:
|
|
if original_llm_agent:
|
|
cleveractors.agents.llm.LLMAgent = original_llm_agent
|
|
|
|
# Test create_agents_from_config
|
|
try:
|
|
with (
|
|
patch("cleveractors.agents.llm.LLMAgent") as mock_llm,
|
|
patch("cleveractors.agents.tool.ToolAgent") as mock_tool,
|
|
):
|
|
mock_llm.return_value = Mock()
|
|
mock_tool.return_value = Mock()
|
|
|
|
all_agents = context.factory.create_agents_from_config()
|
|
context.test_results["all_agents"] = all_agents
|
|
except Exception as e:
|
|
context.test_results["all_agents_error"] = e
|
|
|
|
# Test get_agent_metadata for missing agent
|
|
try:
|
|
missing_metadata = context.factory.get_agent_metadata(
|
|
"completely_missing_agent"
|
|
)
|
|
context.test_results["missing_metadata_error"] = None
|
|
except Exception as e:
|
|
context.test_results["missing_metadata_error"] = e
|
|
|
|
# Test 7: Get agent types
|
|
agent_types_copy = context.factory.get_agent_types()
|
|
original_types = context.factory.agent_types
|
|
context.test_results["types_copy"] = agent_types_copy is not original_types
|
|
|
|
# Test 8: Validation tests
|
|
# Invalid agents config
|
|
invalid_config_factory = factory_module.AgentFactory(
|
|
config={"agents": "not_a_dict"},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
try:
|
|
invalid_config_factory.validate_configuration()
|
|
context.test_results["invalid_agents_validation"] = False
|
|
except Exception as e:
|
|
context.test_results["invalid_agents_validation_error"] = e
|
|
|
|
# Invalid agent config
|
|
invalid_agent_factory = factory_module.AgentFactory(
|
|
config={"agents": {"bad_agent": "not_a_dict"}},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
try:
|
|
invalid_agent_factory.validate_configuration()
|
|
context.test_results["invalid_agent_validation"] = False
|
|
except Exception as e:
|
|
context.test_results["invalid_agent_validation_error"] = e
|
|
|
|
# Missing agent type
|
|
no_type_factory = factory_module.AgentFactory(
|
|
config={"agents": {"no_type": {"config": {}}}},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
try:
|
|
no_type_factory.validate_configuration()
|
|
context.test_results["no_type_validation"] = False
|
|
except Exception as e:
|
|
context.test_results["no_type_validation_error"] = e
|
|
|
|
# Unknown agent type validation
|
|
unknown_type_factory = factory_module.AgentFactory(
|
|
config={"agents": {"unknown": {"type": "unknown_type", "config": {}}}},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
try:
|
|
unknown_type_factory.validate_configuration()
|
|
context.test_results["unknown_type_validation"] = False
|
|
except Exception as e:
|
|
context.test_results["unknown_type_validation_error"] = e
|
|
|
|
# Invalid config section
|
|
invalid_config_section_factory = factory_module.AgentFactory(
|
|
config={"agents": {"bad_config": {"type": "llm", "config": "not_a_dict"}}},
|
|
template_renderer=context.mock_template_renderer,
|
|
)
|
|
try:
|
|
invalid_config_section_factory.validate_configuration()
|
|
context.test_results["invalid_config_section_validation"] = False
|
|
except Exception as e:
|
|
context.test_results["invalid_config_section_validation_error"] = e
|
|
|
|
|
|
@then("comprehensive factory functionality should work correctly")
|
|
def step_comprehensive_factory_functionality_works(context):
|
|
"""Verify comprehensive factory functionality works correctly."""
|
|
assert context.error is None
|
|
|
|
# Test caching
|
|
assert context.test_results["cache_test"] is True
|
|
|
|
# Test metadata for agent without config section
|
|
metadata = context.test_results["no_config_metadata"]
|
|
assert metadata["name"] == "missing_config_test"
|
|
assert metadata["type"] == "llm"
|
|
|
|
# Test unknown type error
|
|
error = context.test_results["unknown_type_error"]
|
|
assert error is not None
|
|
assert isinstance(error, AgentCreationError)
|
|
assert "Unknown agent type" in str(error)
|
|
|
|
# Test tool metadata
|
|
metadata = context.test_results["tool_metadata"]
|
|
assert metadata["type"] == "tool"
|
|
assert metadata["tools"] == ["math", "search"]
|
|
assert metadata["allow_shell"] is True
|
|
assert metadata["safe_mode"] is False
|
|
|
|
# Test valid registration
|
|
assert context.test_results["register_valid"] is True
|
|
|
|
# Test invalid registration
|
|
error = context.test_results["register_invalid_error"]
|
|
assert error is not None
|
|
assert isinstance(error, ConfigurationError)
|
|
assert "inherit from Agent" in str(error)
|
|
|
|
# Test agent types copy
|
|
assert context.test_results["types_copy"] is True
|
|
|
|
# Test missing agent
|
|
error = context.test_results["missing_agent_error"]
|
|
assert error is not None
|
|
assert isinstance(error, AgentCreationError)
|
|
assert "No configuration found" in str(error)
|
|
|
|
# Test legacy composite with non-existent agent — now raises
|
|
# AgentCreationError (previously silently skipped, which was a bug).
|
|
error = context.test_results.get("legacy_missing_agent_error")
|
|
assert error is not None, (
|
|
"Expected AgentCreationError for legacy composite with non-existent "
|
|
"agent, but no error was raised"
|
|
)
|
|
assert isinstance(error, AgentCreationError), (
|
|
f"Expected AgentCreationError, got {type(error).__name__}: {error}"
|
|
)
|
|
assert "Unknown agent" in str(error), (
|
|
f"Error message should mention unknown agent, got: {error!r}"
|
|
)
|
|
|
|
# Test creation failure
|
|
error = context.test_results["creation_failure_error"]
|
|
if error is not None:
|
|
assert isinstance(error, AgentCreationError)
|
|
assert "Failed to create agent" in str(error)
|
|
assert "Mock creation failure" in str(error)
|
|
# If no error, that means the mock wasn't effective, but the test coverage might still be achieved
|
|
|
|
# Test create all agents
|
|
if "all_agents" in context.test_results:
|
|
all_agents = context.test_results["all_agents"]
|
|
assert all_agents is not None
|
|
assert len(all_agents) > 0
|
|
elif "all_agents_error" in context.test_results:
|
|
# The all_agents test had an error, that's okay for coverage purposes
|
|
pass
|
|
|
|
# Test missing metadata
|
|
error = context.test_results["missing_metadata_error"]
|
|
assert error is not None
|
|
assert isinstance(error, AgentCreationError)
|
|
assert "No configuration found" in str(error)
|
|
|
|
# Test validation errors
|
|
assert isinstance(
|
|
context.test_results["invalid_agents_validation_error"], ConfigurationError
|
|
)
|
|
assert "must be a dictionary" in str(
|
|
context.test_results["invalid_agents_validation_error"]
|
|
)
|
|
|
|
assert isinstance(
|
|
context.test_results["invalid_agent_validation_error"], ConfigurationError
|
|
)
|
|
assert "must be a dictionary" in str(
|
|
context.test_results["invalid_agent_validation_error"]
|
|
)
|
|
|
|
assert isinstance(
|
|
context.test_results["no_type_validation_error"], ConfigurationError
|
|
)
|
|
assert "must specify a type" in str(
|
|
context.test_results["no_type_validation_error"]
|
|
)
|
|
|
|
assert isinstance(
|
|
context.test_results["unknown_type_validation_error"], ConfigurationError
|
|
)
|
|
assert "Unknown agent type" in str(
|
|
context.test_results["unknown_type_validation_error"]
|
|
)
|
|
|
|
assert isinstance(
|
|
context.test_results["invalid_config_section_validation_error"],
|
|
ConfigurationError,
|
|
)
|
|
assert "'config'" in str(
|
|
context.test_results["invalid_config_section_validation_error"]
|
|
)
|
|
assert "must be a dictionary" in str(
|
|
context.test_results["invalid_config_section_validation_error"]
|
|
)
|