Files
cleveragents-core/tests/features/steps/tool_agent_steps.py
T

341 lines
12 KiB
Python

import json
import os
from unittest.mock import AsyncMock
from unittest.mock import MagicMock
from unittest.mock import patch
from behave import given
from behave import then
from behave import when
from behave.api.async_step import async_run_until_complete
from cleveragents.agents.tool import ToolAgent
from cleveragents.core.exceptions import AgentCreationError
from cleveragents.core.exceptions import ExecutionError
from cleveragents.templates.renderer import TemplateRenderer
@given('a ToolAgent is configured with name "{name}" and config')
def step_tool_agent_config(context, name):
config = json.loads(context.text)
context.agent_config = config
context.agent_name = name
if not hasattr(context, "template_renderer"):
context.template_renderer = TemplateRenderer()
async def _create_tool_agent_helper(context):
"""Helper function for creating ToolAgent."""
context.error = None
try:
context.agent = ToolAgent(
name=context.agent_name,
config=context.agent_config,
template_renderer=context.template_renderer,
)
# Mock HTTP requests for testing to avoid network calls
if "http_request" in context.agent_config.get("tools", []):
# We'll handle HTTP mocking with aiohttp patches during execution
pass
except (AgentCreationError, Exception) as e:
context.error = e
@when("I create the ToolAgent")
@async_run_until_complete
async def step_create_tool_agent(context):
await _create_tool_agent_helper(context)
@when("I try to create the ToolAgent")
@async_run_until_complete
async def step_try_to_create_tool_agent(context):
# This is an alias for the create step to make scenarios more readable
await _create_tool_agent_helper(context)
@when('I process a message with the ToolAgent: "{message}"')
@async_run_until_complete
async def step_process_message_tool_agent(context, message):
context.error = None
context.result = None
try:
test_context = (
{"_unsafe_mode": context.unsafe} if hasattr(context, "unsafe") else {}
)
context.result = await context.agent.process_message(
message, context=test_context
)
except ExecutionError as e:
context.error = e
async def _process_json_message_helper(context):
"""Helper function for processing JSON messages."""
context.error = None
context.result = None
try:
message = json.loads(context.text)
# for file paths
if "file" in message.get("args", {}):
message["args"]["file"] = os.path.join(
context.scenario_temp, message["args"]["file"]
)
message_str = json.dumps(message)
test_context = (
{"_unsafe_mode": context.unsafe} if hasattr(context, "unsafe") else {}
)
# Mock HTTP requests if this is an HTTP request tool call
if message.get("tool") == "http_request":
url = message.get("args", {}).get("url", "")
# Handle specific error scenarios
if "delay" in url or context.agent_config.get("timeout", 5) <= 0.001:
# Simulate timeout error
async def mock_timeout_session():
import asyncio
raise asyncio.TimeoutError("Simulated timeout")
with patch("aiohttp.ClientSession") as mock_client:
mock_client.return_value.__aenter__ = AsyncMock(
side_effect=mock_timeout_session
)
context.result = await context.agent.process_message(
message_str, context=test_context
)
elif "invalid://" in url:
# Simulate invalid URL error
async def mock_invalid_session():
from aiohttp import InvalidURL
raise InvalidURL("Invalid URL scheme")
with patch("aiohttp.ClientSession") as mock_client:
mock_client.return_value.__aenter__ = AsyncMock(
side_effect=mock_invalid_session
)
context.result = await context.agent.process_message(
message_str, context=test_context
)
else:
# Normal successful response
mock_response = MagicMock()
mock_response.status = 200
mock_response.text = AsyncMock(
return_value='{"success": true, "url": "' + url + '"}'
)
mock_session = MagicMock()
mock_request = AsyncMock()
mock_request.__aenter__ = AsyncMock(return_value=mock_response)
mock_request.__aexit__ = AsyncMock(return_value=None)
mock_session.request.return_value = mock_request
mock_client_session = MagicMock()
mock_client_session.__aenter__ = AsyncMock(return_value=mock_session)
mock_client_session.__aexit__ = AsyncMock(return_value=None)
with patch("aiohttp.ClientSession", return_value=mock_client_session):
context.result = await context.agent.process_message(
message_str, context=test_context
)
else:
context.result = await context.agent.process_message(
message_str, context=test_context
)
except (ExecutionError, json.JSONDecodeError) as e:
context.error = e
@when("I process a JSON message with the ToolAgent")
@async_run_until_complete
async def step_process_json_message_tool_agent_no_params(context):
"""Step definition for processing JSON message without colon."""
await _process_json_message_helper(context)
@when("I process a JSON message with the ToolAgent:")
@async_run_until_complete
async def step_process_json_message_tool_agent(context):
await _process_json_message_helper(context)
@then('the result should be "{expected_result}"')
def step_check_result(context, expected_result):
assert context.result is not None, "Expected a result, but got None"
assert (
str(context.result) == expected_result
), f"Expected '{expected_result}', but got '{context.result}'"
assert context.error is None, f"Expected no error, but got {context.error}"
@then('the result should be ""')
def step_check_empty_result(context):
assert context.result is not None, "Expected a result, but got None"
assert (
str(context.result) == ""
), f"Expected empty string, but got '{context.result}'"
assert context.error is None, f"Expected no error, but got {context.error}"
@then('the tool result should contain "{text}"')
def step_check_result_contains(context, text):
assert context.result is not None, "Expected a result, but got None"
assert text in str(
context.result
), f"Expected result to contain '{text}', but it was '{context.result}'"
assert context.error is None, f"Expected no error, but got {context.error}"
@then('tool execution should fail with message containing "{message}"')
def step_tool_execution_fail(context, message):
assert context.error is not None, "Expected an error, but none was raised"
assert isinstance(
context.error, ExecutionError
), f"Expected ExecutionError, but got {type(context.error)}"
assert message in str(
context.error
), f"Expected error message to contain '{message}', but it was '{str(context.error)}'"
@then('agent creation should fail with message containing "{message}"')
def step_agent_creation_fail(context, message):
assert context.error is not None, "Expected an error, but none was raised"
assert isinstance(
context.error, AgentCreationError
), f"Expected AgentCreationError, but got {type(context.error)}"
assert message in str(
context.error
), f"Expected error message to contain '{message}', but it was '{str(context.error)}'"
@given("I am running in unsafe mode")
def step_run_unsafe(context):
context.unsafe = True
@then('the agent capabilities should contain "{capability}"')
def step_check_capabilities(context, capability):
capabilities = context.agent.get_capabilities()
assert capability in capabilities, f"Expected '{capability}' in {capabilities}"
@then('the agent metadata "{key}" should be {value}')
def step_check_metadata(context, key, value):
metadata = context.agent.get_metadata()
# convert value string to bool/int if possible
if value.lower() == "true":
typed_value = True
elif value.lower() == "false":
typed_value = False
else:
try:
typed_value = int(value)
except ValueError:
typed_value = value # keep as string
assert key in metadata, f"Metadata key '{key}' not found."
assert (
metadata[key] == typed_value
), f"Expected metadata['{key}'] to be {typed_value}, but got {metadata[key]}"
@when("I create a test file with content {content}")
def step_create_test_file(context, content):
"""Create a test file for file operations."""
import os
# Create file in current working directory for the tool to find
filepath = "test_file.txt"
with open(filepath, "w", encoding="utf-8") as f:
f.write(content.strip('"'))
# Clean up after test - use direct dict access to avoid behave context issues
if not hasattr(context, "__dict__") or "_cleanup_files" not in context.__dict__:
context.__dict__["_cleanup_files"] = []
context.__dict__["_cleanup_files"].append(filepath)
@when("I create an absolute test file with content {content}")
def step_create_absolute_test_file(context, content):
"""Create a test file at absolute path for file operations."""
filepath = "/tmp/test_abs_file.txt"
with open(filepath, "w", encoding="utf-8") as f:
f.write(content.strip('"'))
# Clean up after test - use direct dict access to avoid behave context issues
if not hasattr(context, "__dict__") or "_cleanup_files" not in context.__dict__:
context.__dict__["_cleanup_files"] = []
context.__dict__["_cleanup_files"].append(filepath)
@when("I process a JSON message with unsafe context with the ToolAgent")
@async_run_until_complete
async def step_process_json_message_unsafe_context(context):
"""Process JSON message with unsafe context."""
context.error = None
context.result = None
try:
message = json.loads(context.text)
test_context = {"_unsafe_mode": True}
# for file paths - keep absolute paths as is
message_str = json.dumps(message)
context.result = await context.agent.process_message(
message_str, context=test_context
)
except (ExecutionError, json.JSONDecodeError) as e:
context.error = e
@when('I process a message with the ToolAgent: ""')
@async_run_until_complete
async def step_process_empty_message_tool_agent(context):
"""Process an empty message with the ToolAgent."""
context.error = None
context.result = None
try:
test_context = (
{"_unsafe_mode": context.unsafe} if hasattr(context, "unsafe") else {}
)
context.result = await context.agent.process_message("", context=test_context)
except ExecutionError as e:
context.error = e
@when("I process a malformed message causing general exception")
@async_run_until_complete
async def step_process_malformed_message(context):
"""Process a message that causes a general exception."""
context.error = None
context.result = None
try:
# This will cause an exception in the tool processing due to invalid tool name
context.result = await context.agent.process_message("nonexistent_tool")
except ExecutionError as e:
context.error = e
@when("I process a JSON message with unsafe context with the ToolAgent:")
@async_run_until_complete
async def step_process_json_unsafe_context_with_colon(context):
"""Process JSON message with unsafe context using ToolAgent."""
# Call the function implementation directly to avoid nested async issues
import json
context.error = None
context.result = None
try:
message = json.loads(context.text)
test_context = {"_unsafe_mode": True}
# for file paths - keep absolute paths as is
message_str = json.dumps(message)
context.result = await context.agent.process_message(
message_str, context=test_context
)
except Exception as e:
context.error = e