test: fix tool_agent shell command mocking and cleanup and update README.md

This commit is contained in:
2025-10-23 20:39:27 +05:30
parent fb92bf6790
commit 3e6e984567
4 changed files with 113 additions and 14 deletions
-9
View File
@@ -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:
+19 -2
View File
@@ -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}"
+19
View File
@@ -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()
+75 -3
View File
@@ -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