"""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])