forked from cleveragents/cleveragents-core
341 lines
12 KiB
Python
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
|