OpenHands / tests /runtime /test_microagent.py
Backup-bdg's picture
Upload 964 files
51ff9e5 verified
raw
history blame
13.3 kB
"""Tests for microagent loading in runtime."""
import os
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from conftest import (
_close_test_runtime,
_load_runtime,
)
from openhands.core.config import MCPConfig
from openhands.core.config.mcp_config import MCPStdioServerConfig
from openhands.mcp.utils import add_mcp_tools_to_agent
from openhands.microagent.microagent import (
BaseMicroagent,
KnowledgeMicroagent,
RepoMicroagent,
TaskMicroagent,
)
from openhands.microagent.types import MicroagentType
def _create_test_microagents(test_dir: str):
"""Create test microagent files in the given directory."""
microagents_dir = Path(test_dir) / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create test knowledge agent
knowledge_dir = microagents_dir / 'knowledge'
knowledge_dir.mkdir(exist_ok=True)
knowledge_agent = """---
name: test_knowledge_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- test
- pytest
---
# Test Guidelines
Testing best practices and guidelines.
"""
(knowledge_dir / 'knowledge.md').write_text(knowledge_agent)
# Create test repo agent
repo_agent = """---
name: test_repo_agent
type: repo
version: 1.0.0
agent: CodeActAgent
---
# Test Repository Agent
Repository-specific test instructions.
"""
(microagents_dir / 'repo.md').write_text(repo_agent)
# Create legacy repo instructions
legacy_instructions = """# Legacy Instructions
These are legacy repository instructions.
"""
(Path(test_dir) / '.openhands_instructions').write_text(legacy_instructions)
def test_load_microagents_with_trailing_slashes(
temp_dir, runtime_cls, run_as_openhands
):
"""Test loading microagents when directory paths have trailing slashes."""
# Create test files
_create_test_microagents(temp_dir)
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Load microagents
loaded_agents = runtime.get_microagents_from_selected_repo(None)
# Verify all agents are loaded
knowledge_agents = [
a for a in loaded_agents if isinstance(a, KnowledgeMicroagent)
]
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroagent)]
# Check knowledge agents
assert len(knowledge_agents) == 1
agent = knowledge_agents[0]
assert agent.name == 'knowledge/knowledge'
assert 'test' in agent.triggers
assert 'pytest' in agent.triggers
# Check repo agents (including legacy)
assert len(repo_agents) == 2 # repo.md + .openhands_instructions
repo_names = {a.name for a in repo_agents}
assert 'repo' in repo_names
assert 'repo_legacy' in repo_names
finally:
_close_test_runtime(runtime)
def test_load_microagents_with_selected_repo(temp_dir, runtime_cls, run_as_openhands):
"""Test loading microagents from a selected repository."""
# Create test files in a repository-like structure
repo_dir = Path(temp_dir) / 'OpenHands'
repo_dir.mkdir(parents=True)
_create_test_microagents(str(repo_dir))
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Load microagents with selected repository
loaded_agents = runtime.get_microagents_from_selected_repo(
'All-Hands-AI/OpenHands'
)
# Verify all agents are loaded
knowledge_agents = [
a for a in loaded_agents if isinstance(a, KnowledgeMicroagent)
]
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroagent)]
# Check knowledge agents
assert len(knowledge_agents) == 1
agent = knowledge_agents[0]
assert agent.name == 'knowledge/knowledge'
assert 'test' in agent.triggers
assert 'pytest' in agent.triggers
# Check repo agents (including legacy)
assert len(repo_agents) == 2 # repo.md + .openhands_instructions
repo_names = {a.name for a in repo_agents}
assert 'repo' in repo_names
assert 'repo_legacy' in repo_names
finally:
_close_test_runtime(runtime)
def test_load_microagents_with_missing_files(temp_dir, runtime_cls, run_as_openhands):
"""Test loading microagents when some files are missing."""
# Create only repo.md, no other files
microagents_dir = Path(temp_dir) / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
repo_agent = """---
name: test_repo_agent
type: repo
version: 1.0.0
agent: CodeActAgent
---
# Test Repository Agent
Repository-specific test instructions.
"""
(microagents_dir / 'repo.md').write_text(repo_agent)
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Load microagents
loaded_agents = runtime.get_microagents_from_selected_repo(None)
# Verify only repo agent is loaded
knowledge_agents = [
a for a in loaded_agents if isinstance(a, KnowledgeMicroagent)
]
repo_agents = [a for a in loaded_agents if isinstance(a, RepoMicroagent)]
assert len(knowledge_agents) == 0
assert len(repo_agents) == 1
agent = repo_agents[0]
assert agent.name == 'repo'
finally:
_close_test_runtime(runtime)
def test_task_microagent_creation():
"""Test that a TaskMicroagent is created correctly."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent with a variable: ${test_var}.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.type == MicroagentType.TASK
assert agent.name == 'test_task'
assert '/test_task' in agent.triggers
assert "If the user didn't provide any of these variables" in agent.content
def test_task_microagent_variable_extraction():
"""Test that variables are correctly extracted from the content."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: var1
description: "Variable 1"
---
This is a test with variables: ${var1}, ${var2}, and ${var3}.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
variables = agent.extract_variables(agent.content)
assert set(variables) == {'var1', 'var2', 'var3'}
assert agent.requires_user_input()
def test_knowledge_microagent_no_prompt():
"""Test that a regular KnowledgeMicroagent doesn't get the prompt."""
content = """---
name: test_knowledge
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- test_knowledge
---
This is a test knowledge microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, KnowledgeMicroagent)
assert agent.type == MicroagentType.KNOWLEDGE
assert "If the user didn't provide any of these variables" not in agent.content
def test_task_microagent_trigger_addition():
"""Test that a trigger is added if not present."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert '/test_task' in agent.triggers
def test_task_microagent_no_duplicate_trigger():
"""Test that a trigger is not duplicated if already present."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
- another_trigger
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.triggers.count('/test_task') == 1 # No duplicates
assert len(agent.triggers) == 2
assert 'another_trigger' in agent.triggers
assert '/test_task' in agent.triggers
def test_task_microagent_match_trigger():
"""Test that a task microagent matches its trigger correctly."""
content = """---
name: test_task
version: 1.0.0
author: openhands
agent: CodeActAgent
triggers:
- /test_task
inputs:
- name: TEST_VAR
description: "Test variable"
---
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.match_trigger('/test_task') == '/test_task'
assert agent.match_trigger(' /test_task ') == '/test_task'
assert agent.match_trigger('This contains /test_task') == '/test_task'
assert agent.match_trigger('/other_task') is None
def test_default_tools_microagent_exists():
"""Test that the default-tools microagent exists in the global microagents directory."""
# Get the path to the global microagents directory
import openhands
project_root = os.path.dirname(openhands.__file__)
parent_dir = os.path.dirname(project_root)
microagents_dir = os.path.join(parent_dir, 'microagents')
# Check that the default-tools.md file exists
default_tools_path = os.path.join(microagents_dir, 'default-tools.md')
assert os.path.exists(default_tools_path), (
f'default-tools.md not found at {default_tools_path}'
)
# Read the file and check its content
with open(default_tools_path, 'r') as f:
content = f.read()
# Verify it's a repo microagent (always activated)
assert 'type: repo' in content, 'default-tools.md should be a repo microagent'
# Verify it has the fetch tool configured
assert 'name: "fetch"' in content, 'default-tools.md should have a fetch tool'
assert 'command: "uvx"' in content, 'default-tools.md should use uvx command'
assert 'args: ["mcp-server-fetch"]' in content, (
'default-tools.md should use mcp-server-fetch'
)
@pytest.mark.asyncio
async def test_add_mcp_tools_from_microagents():
"""Test that add_mcp_tools_to_agent adds tools from microagents."""
# Import ActionExecutionClient for mocking
from openhands.core.config.openhands_config import OpenHandsConfig
from openhands.runtime.impl.action_execution.action_execution_client import (
ActionExecutionClient,
)
# Create mock objects
mock_agent = MagicMock()
mock_runtime = MagicMock(spec=ActionExecutionClient)
mock_memory = MagicMock()
mock_mcp_config = MCPConfig()
# Create a mock OpenHandsConfig with the MCP config
mock_app_config = OpenHandsConfig(mcp=mock_mcp_config, search_api_key=None)
# Configure the mock memory to return a microagent MCP config
mock_stdio_server = MCPStdioServerConfig(
name='test-tool', command='test-command', args=['test-arg1', 'test-arg2']
)
mock_microagent_mcp_config = MCPConfig(stdio_servers=[mock_stdio_server])
mock_memory.get_microagent_mcp_tools.return_value = [mock_microagent_mcp_config]
# Configure the mock runtime
mock_runtime.runtime_initialized = True
mock_runtime.get_mcp_config.return_value = mock_microagent_mcp_config
# Mock the fetch_mcp_tools_from_config function to return a mock tool
mock_tool = {
'type': 'function',
'function': {
'name': 'test-tool',
'description': 'Test tool description',
'parameters': {},
},
}
with patch(
'openhands.mcp.utils.fetch_mcp_tools_from_config',
new=AsyncMock(return_value=[mock_tool]),
):
# Call the function with the OpenHandsConfig instead of MCPConfig
await add_mcp_tools_to_agent(
mock_agent, mock_runtime, mock_memory, mock_app_config
)
# Verify that the memory's get_microagent_mcp_tools was called
mock_memory.get_microagent_mcp_tools.assert_called_once()
# Verify that the runtime's get_mcp_config was called with the extra stdio servers
mock_runtime.get_mcp_config.assert_called_once()
args, kwargs = mock_runtime.get_mcp_config.call_args
assert len(args) == 1
assert len(args[0]) == 1
assert args[0][0].name == 'test-tool'
# Verify that the agent's set_mcp_tools was called with the mock tool
mock_agent.set_mcp_tools.assert_called_once_with([mock_tool])