forked from HAL9000/cleveragents-core
test: fix tool_agent shell command mocking and cleanup and update README.md
This commit is contained in:
@@ -32,15 +32,6 @@ cd cleveragents-core
|
||||
pip install -e .
|
||||
|
||||
```
|
||||
|
||||
### Install from Source
|
||||
|
||||
```bash
|
||||
git clone https://git.cleverthis.com/cleveragents/cleveragents-core
|
||||
cd cleveragents-core
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
CleverAgents automatically installs these dependencies:
|
||||
|
||||
@@ -306,8 +306,12 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
try:
|
||||
self.logger.info("Starting reactive interactive session")
|
||||
|
||||
# Create a variable to track completion of each message
|
||||
completion_future: Optional[asyncio.Future] = None
|
||||
|
||||
# Set up observers for output and errors
|
||||
def on_output(msg: StreamMessage) -> None:
|
||||
nonlocal completion_future # Access the outer variable
|
||||
content_str = str(msg.content)
|
||||
|
||||
if not content_str or content_str.strip() == "":
|
||||
@@ -317,6 +321,10 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
processed_content = self._process_tool_commands(content_str)
|
||||
print(f"\n{processed_content}\n")
|
||||
|
||||
# Signal completion
|
||||
if completion_future and not completion_future.done():
|
||||
completion_future.set_result(True)
|
||||
|
||||
def on_error(msg: StreamMessage) -> None:
|
||||
print(f"\n[ERROR] {msg.content}")
|
||||
|
||||
@@ -355,10 +363,16 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
metadata: Dict[str, Any] = {"context": self.config.global_context}
|
||||
metadata["_unsafe_mode"] = self.unsafe
|
||||
|
||||
# Create a new future for this message
|
||||
completion_future = asyncio.Future()
|
||||
|
||||
self.stream_router.send_message("__input__", user_input, metadata)
|
||||
|
||||
# Give streams time to process
|
||||
await asyncio.sleep(2.0)
|
||||
# Wait for actual completion (not a guess!)
|
||||
try:
|
||||
await asyncio.wait_for(completion_future, timeout=30.0) # Safety timeout
|
||||
except asyncio.TimeoutError:
|
||||
print("\n[WARN] Stream processing timeout\n")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nUse 'exit' to quit.")
|
||||
@@ -754,6 +768,7 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
context = {"_unsafe_mode": self.unsafe}
|
||||
|
||||
# Execute based on whether we're in an async context
|
||||
result = None
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
# Already in async context, use thread pool
|
||||
@@ -765,6 +780,8 @@ class ReactiveCleverAgentsApp: # pylint: disable=too-many-instance-attributes
|
||||
except RuntimeError:
|
||||
# No running loop, use asyncio.run
|
||||
result = asyncio.run(target_agent.process_message(tool_request, context))
|
||||
if result is None:
|
||||
return "\n❌ Error: Tool execution produced no result"
|
||||
|
||||
return f"\n✅ {result}"
|
||||
|
||||
|
||||
@@ -227,6 +227,25 @@ def after_scenario(context, scenario):
|
||||
|
||||
cleanup_test_files(context)
|
||||
|
||||
# Clean up files tracked for cleanup in tool_agent tests
|
||||
if hasattr(context, "__dict__") and "_cleanup_files" in context.__dict__:
|
||||
import os
|
||||
for filepath in context.__dict__["_cleanup_files"]:
|
||||
try:
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
except Exception:
|
||||
pass
|
||||
context.__dict__["_cleanup_files"] = []
|
||||
|
||||
# Clean up scenario temp directory
|
||||
if hasattr(context, "scenario_temp") and context.scenario_temp.exists():
|
||||
import shutil
|
||||
try:
|
||||
shutil.rmtree(context.scenario_temp, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clean up test context for InlineYAMLJinja tests - not needed for simplified tests
|
||||
# if hasattr(context, 'test_context'):
|
||||
# context.test_context.cleanup()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import AsyncMock
|
||||
@@ -63,9 +64,80 @@ async def step_process_message_tool_agent(context, message):
|
||||
test_context = (
|
||||
{"_unsafe_mode": context.unsafe} if hasattr(context, "unsafe") else {}
|
||||
)
|
||||
context.result = await context.agent.process_message(
|
||||
message, context=test_context
|
||||
)
|
||||
|
||||
# Mock shell commands for consistent testing across different systems
|
||||
if context.agent_config.get("allow_shell", False) and message.strip():
|
||||
# Check if this looks like a shell command execution
|
||||
parts = message.strip().split()
|
||||
first_part = parts[0] if parts else ""
|
||||
|
||||
# Commands that should be mocked
|
||||
should_mock = (
|
||||
"/bin/" in first_part or
|
||||
"/usr/bin/" in first_part or
|
||||
first_part in ["sleep", "false", "rm"]
|
||||
)
|
||||
|
||||
if should_mock:
|
||||
# Mock subprocess execution
|
||||
with patch("asyncio.create_subprocess_exec") as mock_subprocess:
|
||||
# Determine the mock behavior based on the command
|
||||
if "echo" in first_part:
|
||||
# Mock echo command - return the arguments
|
||||
mock_output = " ".join(parts[1:])
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = 0
|
||||
mock_process.communicate = AsyncMock(
|
||||
return_value=(mock_output.encode(), b"")
|
||||
)
|
||||
mock_subprocess.return_value = mock_process
|
||||
elif "sleep" in first_part:
|
||||
# Mock sleep that takes too long (will trigger timeout)
|
||||
async def timeout_sleep(*args, **kwargs):
|
||||
await asyncio.sleep(10) # Will be caught by timeout
|
||||
return (b"", b"")
|
||||
mock_process = MagicMock()
|
||||
mock_process.communicate = timeout_sleep
|
||||
mock_subprocess.return_value = mock_process
|
||||
elif "false" in first_part:
|
||||
# Mock command that returns non-zero exit code
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = 1
|
||||
mock_process.communicate = AsyncMock(
|
||||
return_value=(b"", b"")
|
||||
)
|
||||
mock_subprocess.return_value = mock_process
|
||||
elif "rm" in first_part:
|
||||
# rm commands should be blocked by safe mode before execution
|
||||
# But if we get here, mock it to fail
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = 1
|
||||
mock_process.communicate = AsyncMock(
|
||||
return_value=(b"", b"Permission denied")
|
||||
)
|
||||
mock_subprocess.return_value = mock_process
|
||||
else:
|
||||
# Default mock for other commands
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = 0
|
||||
mock_process.communicate = AsyncMock(
|
||||
return_value=(b"", b"")
|
||||
)
|
||||
mock_subprocess.return_value = mock_process
|
||||
|
||||
context.result = await context.agent.process_message(
|
||||
message, context=test_context
|
||||
)
|
||||
else:
|
||||
# Not a shell command, process normally
|
||||
context.result = await context.agent.process_message(
|
||||
message, context=test_context
|
||||
)
|
||||
else:
|
||||
# No shell support or empty message, process normally
|
||||
context.result = await context.agent.process_message(
|
||||
message, context=test_context
|
||||
)
|
||||
except ExecutionError as e:
|
||||
context.error = e
|
||||
|
||||
|
||||
Reference in New Issue
Block a user