"""Tests for GitLab alternative directory support for microagents.""" import tempfile from pathlib import Path from unittest.mock import MagicMock, patch import pytest from openhands.core.config import OpenHandsConfig, SandboxConfig from openhands.events import EventStream from openhands.integrations.service_types import ProviderType, Repository from openhands.llm.llm_registry import LLMRegistry from openhands.microagent.microagent import ( RepoMicroagent, ) from openhands.runtime.base import Runtime from openhands.storage import get_file_store class MockRuntime(Runtime): """Mock runtime for testing.""" def __init__(self, workspace_root: Path): # Create a minimal config for testing config = OpenHandsConfig() config.workspace_mount_path_in_sandbox = str(workspace_root) config.sandbox = SandboxConfig() # Create a mock event stream and file store file_store = get_file_store('local', str(workspace_root)) event_stream = MagicMock(spec=EventStream) event_stream.file_store = file_store # Create a mock LLM registry llm_registry = LLMRegistry(config) # Initialize the parent class properly super().__init__( config=config, event_stream=event_stream, llm_registry=llm_registry, sid='test', git_provider_tokens={}, ) self._workspace_root = workspace_root self._logs = [] @property def workspace_root(self) -> Path: """Return the workspace root path.""" return self._workspace_root def log(self, level: str, message: str): """Mock log method.""" self._logs.append((level, message)) def run_action(self, action): """Mock run_action method.""" # For testing, we'll simulate successful cloning from openhands.events.observation import CmdOutputObservation return CmdOutputObservation(content='', exit_code=0) def read(self, action): """Mock read method.""" from openhands.events.observation import ErrorObservation return ErrorObservation('File not found') def _load_microagents_from_directory(self, directory: Path, source: str): """Mock microagent loading.""" if not directory.exists(): return [] # Create mock microagents based on directory structure microagents = [] for md_file in directory.rglob('*.md'): if md_file.name == 'README.md': continue # Create a simple mock microagent from openhands.microagent.types import MicroagentMetadata, MicroagentType agent = RepoMicroagent( name=f'mock_{md_file.stem}', content=f'Mock content from {md_file}', metadata=MicroagentMetadata(name=f'mock_{md_file.stem}'), source=str(md_file), type=MicroagentType.REPO_KNOWLEDGE, ) microagents.append(agent) return microagents # Implement abstract methods with minimal functionality def connect(self): pass def run(self, action): from openhands.events.observation import CmdOutputObservation return CmdOutputObservation(content='', exit_code=0) def run_ipython(self, action): from openhands.events.observation import IPythonRunCellObservation return IPythonRunCellObservation(content='', code='') def edit(self, action): from openhands.events.observation import FileEditObservation return FileEditObservation(content='', path='') def browse(self, action): from openhands.events.observation import BrowserObservation return BrowserObservation(content='', url='', screenshot='') def browse_interactive(self, action): from openhands.events.observation import BrowserObservation return BrowserObservation(content='', url='', screenshot='') def write(self, action): from openhands.events.observation import FileWriteObservation return FileWriteObservation(content='', path='') def copy_to(self, host_src, sandbox_dest, recursive=False): pass def copy_from(self, sandbox_src, host_dest, recursive=False): pass def list_files(self, path=None): return [] def get_mcp_config(self, extra_stdio_servers=None): from openhands.core.config.mcp_config import MCPConfig return MCPConfig() def call_tool_mcp(self, action): from openhands.events.observation import MCPObservation return MCPObservation(content='', tool='', result='') def create_test_microagents(base_dir: Path, config_dir_name: str = '.openhands'): """Create test microagent files in the specified directory.""" microagents_dir = base_dir / config_dir_name / 'microagents' microagents_dir.mkdir(parents=True, exist_ok=True) # Create a test microagent test_agent = """--- name: test_agent type: repo version: 1.0.0 agent: CodeActAgent --- # Test Agent This is a test microagent. """ (microagents_dir / 'test.md').write_text(test_agent) return microagents_dir @pytest.fixture def temp_workspace(): """Create a temporary workspace directory.""" with tempfile.TemporaryDirectory() as temp_dir: yield Path(temp_dir) def test_is_gitlab_repository_github(temp_workspace): """Test that GitHub repositories are correctly identified as non-GitLab.""" runtime = MockRuntime(temp_workspace) # Mock the provider handler to return GitHub mock_repo = Repository( id='123', full_name='owner/repo', git_provider=ProviderType.GITHUB, is_public=True, ) with patch('openhands.runtime.base.ProviderHandler') as mock_handler_class: mock_handler = MagicMock() mock_handler_class.return_value = mock_handler with patch('openhands.runtime.base.call_async_from_sync') as mock_async: mock_async.return_value = mock_repo result = runtime._is_gitlab_repository('github.com/owner/repo') assert result is False def test_is_gitlab_repository_gitlab(temp_workspace): """Test that GitLab repositories are correctly identified.""" runtime = MockRuntime(temp_workspace) # Mock the provider handler to return GitLab mock_repo = Repository( id='456', full_name='owner/repo', git_provider=ProviderType.GITLAB, is_public=True, ) with patch('openhands.runtime.base.ProviderHandler') as mock_handler_class: mock_handler = MagicMock() mock_handler_class.return_value = mock_handler with patch('openhands.runtime.base.call_async_from_sync') as mock_async: mock_async.return_value = mock_repo result = runtime._is_gitlab_repository('gitlab.com/owner/repo') assert result is True def test_is_gitlab_repository_exception(temp_workspace): """Test that exceptions in provider detection return False.""" runtime = MockRuntime(temp_workspace) with patch('openhands.runtime.base.ProviderHandler') as mock_handler_class: mock_handler_class.side_effect = Exception('Provider error') result = runtime._is_gitlab_repository('unknown.com/owner/repo') assert result is False def test_get_microagents_from_org_or_user_github(temp_workspace): """Test that GitHub repositories only try .openhands directory.""" runtime = MockRuntime(temp_workspace) # Mock the provider detection to return GitHub with patch.object(runtime, '_is_gitlab_repository', return_value=False): # Mock the _get_authenticated_git_url to simulate failure (no org repo) with patch('openhands.runtime.base.call_async_from_sync') as mock_async: mock_async.side_effect = Exception('Repository not found') result = runtime.get_microagents_from_org_or_user('github.com/owner/repo') # Should only try .openhands, not openhands-config assert len(result) == 0 # Check that only one attempt was made (for .openhands) assert mock_async.call_count == 1 def test_get_microagents_from_org_or_user_gitlab_success_with_config(temp_workspace): """Test that GitLab repositories use openhands-config and succeed.""" runtime = MockRuntime(temp_workspace) # Create a mock org directory with microagents org_dir = temp_workspace / 'org_openhands_owner' create_test_microagents(org_dir, '.') # Create microagents directly in org_dir # Mock the provider detection to return GitLab with patch.object(runtime, '_is_gitlab_repository', return_value=True): # Mock successful cloning for openhands-config with patch('openhands.runtime.base.call_async_from_sync') as mock_async: mock_async.return_value = 'https://gitlab.com/owner/openhands-config.git' result = runtime.get_microagents_from_org_or_user('gitlab.com/owner/repo') # Should succeed with openhands-config assert len(result) >= 0 # May be empty if no microagents found # Should only try once for openhands-config assert mock_async.call_count == 1 def test_get_microagents_from_org_or_user_gitlab_failure(temp_workspace): """Test that GitLab repositories handle failure gracefully when openhands-config doesn't exist.""" runtime = MockRuntime(temp_workspace) # Mock the provider detection to return GitLab with patch.object(runtime, '_is_gitlab_repository', return_value=True): # Mock the _get_authenticated_git_url to fail for openhands-config with patch('openhands.runtime.base.call_async_from_sync') as mock_async: mock_async.side_effect = Exception('openhands-config not found') result = runtime.get_microagents_from_org_or_user('gitlab.com/owner/repo') # Should return empty list when repository doesn't exist assert len(result) == 0 # Should only try once for openhands-config assert mock_async.call_count == 1 def test_get_microagents_from_selected_repo_gitlab_uses_openhands(temp_workspace): """Test that GitLab repositories use .openhands directory for repository-specific microagents.""" runtime = MockRuntime(temp_workspace) # Create a repository directory structure repo_dir = temp_workspace / 'repo' repo_dir.mkdir() # Create microagents in .openhands directory create_test_microagents(repo_dir, '.openhands') # Mock the provider detection to return GitLab with patch.object(runtime, '_is_gitlab_repository', return_value=True): # Mock org-level microagents (empty) with patch.object(runtime, 'get_microagents_from_org_or_user', return_value=[]): result = runtime.get_microagents_from_selected_repo('gitlab.com/owner/repo') # Should find microagents from .openhands directory # The exact assertion depends on the mock implementation # At minimum, it should not raise an exception assert isinstance(result, list) def test_get_microagents_from_selected_repo_github_only_openhands(temp_workspace): """Test that GitHub repositories only check .openhands directory.""" runtime = MockRuntime(temp_workspace) # Create a repository directory structure repo_dir = temp_workspace / 'repo' repo_dir.mkdir() # Create microagents in both directories create_test_microagents(repo_dir, 'openhands-config') create_test_microagents(repo_dir, '.openhands') # Mock the provider detection to return GitHub with patch.object(runtime, '_is_gitlab_repository', return_value=False): # Mock org-level microagents (empty) with patch.object(runtime, 'get_microagents_from_org_or_user', return_value=[]): result = runtime.get_microagents_from_selected_repo('github.com/owner/repo') # Should only check .openhands directory, not openhands-config assert isinstance(result, list)