forked from HAL9000/cleveragents-core
156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
"""
|
|
Additional coverage tests for agents/tool.py
|
|
|
|
Covers missing lines for tool agent edge cases and error handling.
|
|
|
|
Coverage targets:
|
|
- Lines 108, 119, 129: JSON extraction returns None for non-dict types
|
|
- Lines 180, 205-212, 228-231: Shell command and tool validation errors
|
|
- Lines 326-327: HTTP tool edge cases
|
|
- Lines 417-418: Math tool edge cases
|
|
"""
|
|
|
|
import pytest
|
|
from cleveragents.agents.tool import ToolAgent
|
|
from cleveragents.core.exceptions import ExecutionError
|
|
from cleveragents.templates.renderer import TemplateRenderer, TemplateEngine
|
|
|
|
|
|
class TestToolAgentJSONExtractionEdgeCases:
|
|
"""Test JSON extraction edge cases (lines 108, 119, 129)."""
|
|
|
|
def test_extract_json_non_dict_returns_none(self):
|
|
"""Test extracting non-dict JSON returns None (line 108)."""
|
|
config = {"tools": ["echo"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# JSON array instead of object
|
|
message = '["item1", "item2", "item3"]'
|
|
result = agent._extract_json_from_message(message)
|
|
|
|
# Should return None for non-dict JSON
|
|
assert result is None
|
|
|
|
def test_extract_json_code_block_non_dict_returns_none(self):
|
|
"""Test extracting non-dict from code block returns None (line 119)."""
|
|
config = {"tools": ["echo"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Code block with array instead of object
|
|
message = '```json\n[1, 2, 3]\n```'
|
|
result = agent._extract_json_from_message(message)
|
|
|
|
# Should return None for non-dict
|
|
assert result is None
|
|
|
|
def test_extract_json_pattern_non_dict_returns_none(self):
|
|
"""Test pattern match non-dict returns None (line 129)."""
|
|
config = {"tools": ["echo"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Text with embedded array
|
|
message = 'Some text [1, 2, 3] more text'
|
|
result = agent._extract_json_from_message(message)
|
|
|
|
# Should return None or not find it
|
|
assert result is None or isinstance(result, dict)
|
|
|
|
|
|
class TestToolAgentShellCommandValidation:
|
|
"""Test shell command validation (lines 180, 205-212, 228-231)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dangerous_command_blocked_in_safe_mode(self):
|
|
"""Test dangerous commands are blocked in safe mode (line 180)."""
|
|
config = {
|
|
"tools": ["shell"],
|
|
"allow_shell": True,
|
|
"safe_mode": True
|
|
}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Dangerous command should be blocked
|
|
with pytest.raises(ExecutionError, match="blocked in safe mode"):
|
|
await agent._execute_shell_command("rm", {"args": ["-rf", "/"]})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_not_in_allowed_list_raises_error(self):
|
|
"""Test executing tool not in allowed list raises error (lines 205-212)."""
|
|
config = {"tools": ["echo", "math"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Try to execute tool not in allowed list
|
|
with pytest.raises(ExecutionError, match="not in allowed tools list"):
|
|
await agent._execute_tool("forbidden_tool", {}, None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_shell_execution_disabled_raises_error(self):
|
|
"""Test shell execution when not allowed raises error (lines 228-231)."""
|
|
config = {
|
|
"tools": ["echo"],
|
|
"allow_shell": False # Shell disabled
|
|
}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Try to execute unknown tool (not builtin, shell disabled)
|
|
with pytest.raises(ExecutionError, match="not in allowed tools list"):
|
|
await agent._execute_tool("unknown_tool", {}, None)
|
|
|
|
|
|
class TestToolAgentHTTPToolEdgeCases:
|
|
"""Test HTTP tool edge cases (lines 326-327)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_http_request_without_url_raises_error(self):
|
|
"""Test HTTP request without URL raises error (line 327)."""
|
|
config = {"tools": ["http_request"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# HTTP request without URL should fail
|
|
args = {"method": "GET"} # Missing URL
|
|
|
|
with pytest.raises(ExecutionError, match="requires a URL"):
|
|
await agent._http_request_tool(args, None)
|
|
|
|
|
|
class TestToolAgentMathToolEdgeCases:
|
|
"""Test math tool edge cases (lines 417-418)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_math_tool_without_expression_raises_error(self):
|
|
"""Test math tool without expression raises error (line 417)."""
|
|
config = {"tools": ["math"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Math without expression should fail
|
|
args = {} # No expression provided
|
|
|
|
with pytest.raises(ExecutionError, match="requires an expression"):
|
|
await agent._math_tool(args, None)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_math_tool_with_args_list(self):
|
|
"""Test math tool with expression in args list (line 418)."""
|
|
config = {"tools": ["math"]}
|
|
renderer = TemplateRenderer(TemplateEngine.JINJA2)
|
|
agent = ToolAgent("test", config, renderer)
|
|
|
|
# Math with expression in args list format
|
|
args = {"args": ["5 * 5"]}
|
|
result = await agent._math_tool(args, None)
|
|
|
|
# Should evaluate the expression
|
|
assert result == "25"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|