Spaces:
Build error
Build error
| """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' | |
| ) | |
| 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]) | |