OpenHands/tests/unit/test_get_repository_microagents.py

1174 lines
41 KiB
Python

import os
import shutil
import tempfile
from datetime import datetime
from pathlib import Path
from types import MappingProxyType
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import SecretStr
from openhands.core.config.mcp_config import MCPConfig, MCPStdioServerConfig
from openhands.integrations.provider import ProviderToken, ProviderType
from openhands.integrations.service_types import (
AuthenticationError,
Repository,
)
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.types import InputMetadata, MicroagentMetadata, MicroagentType
from openhands.server.routes.git import app as git_app
from openhands.server.user_auth import (
get_access_token,
get_provider_tokens,
get_user_id,
)
@pytest.fixture
def test_client():
"""Create a test client for the git API."""
app = FastAPI()
app.include_router(git_app)
# Mock SESSION_API_KEY to None to disable authentication in tests
with patch.dict(os.environ, {'SESSION_API_KEY': ''}, clear=False):
# Clear the SESSION_API_KEY to disable auth dependency
with patch('openhands.server.dependencies._SESSION_API_KEY', None):
# Override the FastAPI dependencies directly
def mock_get_provider_tokens():
return MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(
token=SecretStr('ghp_test_token'), host='github.com'
)
}
)
def mock_get_access_token():
return None
def mock_get_user_id():
return 'test_user'
# Override the dependencies in the app
app.dependency_overrides[get_provider_tokens] = mock_get_provider_tokens
app.dependency_overrides[get_access_token] = mock_get_access_token
app.dependency_overrides[get_user_id] = mock_get_user_id
yield TestClient(app)
@pytest.fixture
def mock_provider_tokens():
"""Create mock provider tokens for testing."""
return MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(
token=SecretStr('ghp_test_token'), host='github.com'
),
ProviderType.GITLAB: ProviderToken(
token=SecretStr('glpat_test_token'), host='gitlab.com'
),
ProviderType.BITBUCKET: ProviderToken(
token=SecretStr('test_token'), host='bitbucket.org'
),
}
)
@pytest.fixture
def mock_repo_microagent():
"""Create a mock repository microagent."""
return RepoMicroagent(
name='test_repo_agent',
content='This is a test repository microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_repo_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[
InputMetadata(
name='query',
type='str',
description='Search query for the repository',
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='git', command='git'),
MCPStdioServerConfig(name='file_editor', command='editor'),
]
),
),
source='.openhands/microagents/test_repo_agent.md',
type=MicroagentType.REPO_KNOWLEDGE,
)
@pytest.fixture
def mock_knowledge_microagent():
"""Create a mock knowledge microagent."""
return KnowledgeMicroagent(
name='test_knowledge_agent',
content='This is a test knowledge microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_knowledge_agent',
type=MicroagentType.KNOWLEDGE,
inputs=[
InputMetadata(
name='topic', type='str', description='Topic to search for'
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='search', command='search'),
MCPStdioServerConfig(name='fetch', command='fetch'),
]
),
),
source='.openhands/microagents/test_knowledge_agent.md',
type=MicroagentType.KNOWLEDGE,
triggers=['test', 'knowledge', 'search'],
)
@pytest.fixture
def temp_microagents_dir():
"""Create a temporary directory with microagents for testing."""
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create sample microagent files
repo_agent_file = microagents_dir / 'test_repo_agent.md'
repo_agent_file.write_text(
"""---
name: test_repo_agent
type: repo_knowledge
inputs:
- name: query
type: str
description: Search query for the repository
mcp_tools:
stdio_servers:
- name: git
command: git
- name: file_editor
command: editor
---
This is a test repository microagent for testing purposes.
"""
)
knowledge_agent_file = microagents_dir / 'test_knowledge_agent.md'
knowledge_agent_file.write_text(
"""---
name: test_knowledge_agent
type: knowledge
triggers: [test, knowledge, search]
inputs:
- name: topic
type: str
description: Topic to search for
mcp_tools:
stdio_servers:
- name: search
command: search
- name: fetch
command: fetch
---
This is a test knowledge microagent for testing purposes.
"""
)
yield temp_dir
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.fixture
def mock_repository():
"""Create a mock repository for testing."""
return Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
@pytest.fixture
def mock_provider_handler(mock_repository):
"""Create a mock provider handler for testing."""
handler = MagicMock()
handler.verify_repo_provider = AsyncMock(return_value=mock_repository)
handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
return handler
@pytest.fixture
def mock_subprocess_result():
"""Create a mock subprocess result for testing."""
result = MagicMock()
result.returncode = 0
result.stderr = ''
return result
@pytest.fixture
def mock_microagents_data(mock_repo_microagent, mock_knowledge_microagent):
"""Create mock microagents data for testing."""
return (
{'test_repo_agent': mock_repo_microagent},
{'test_knowledge_agent': mock_knowledge_microagent},
)
class TestGetRepositoryMicroagents:
"""Test cases for the get_repository_microagents API endpoint."""
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_success(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_provider_tokens,
mock_repo_microagent,
mock_knowledge_microagent,
temp_microagents_dir,
mock_microagents_data,
):
"""Test successful retrieval of microagents from a repository."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create mock microagents with proper absolute paths
repo_agent_with_path = RepoMicroagent(
name='test_repo_agent',
content='This is a test repository microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_repo_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[
InputMetadata(
name='query',
type='str',
description='Search query for the repository',
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='git', command='git'),
MCPStdioServerConfig(name='file_editor', command='editor'),
]
),
),
source=str(
Path(temp_microagents_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'test_repo_agent.md'
),
type=MicroagentType.REPO_KNOWLEDGE,
)
knowledge_agent_with_path = KnowledgeMicroagent(
name='test_knowledge_agent',
content='This is a test knowledge microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_knowledge_agent',
type=MicroagentType.KNOWLEDGE,
inputs=[
InputMetadata(
name='topic', type='str', description='Topic to search for'
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='search', command='search'),
MCPStdioServerConfig(name='fetch', command='fetch'),
]
),
),
source=str(
Path(temp_microagents_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'test_knowledge_agent.md'
),
type=MicroagentType.KNOWLEDGE,
triggers=['test', 'knowledge', 'search'],
)
mock_microagents_data_with_paths = (
{'test_repo_agent': repo_agent_with_path},
{'test_knowledge_agent': knowledge_agent_with_path},
)
mock_load_microagents.return_value = mock_microagents_data_with_paths
mock_mkdtemp.return_value = temp_microagents_dir
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 2
# Check repo microagent
repo_agent = next(m for m in data if m['name'] == 'test_repo_agent')
assert repo_agent['type'] == 'repo'
assert (
repo_agent['content']
== 'This is a test repository microagent for testing purposes.'
)
assert repo_agent['triggers'] == []
assert len(repo_agent['inputs']) == 1
assert repo_agent['inputs'][0]['name'] == 'query'
assert repo_agent['tools'] == ['git', 'file_editor']
assert 'created_at' in repo_agent
assert 'git_provider' in repo_agent
assert repo_agent['git_provider'] == 'github'
assert 'path' in repo_agent
assert repo_agent['path'] == '.openhands/microagents/test_repo_agent.md'
# Check knowledge microagent
knowledge_agent = next(m for m in data if m['name'] == 'test_knowledge_agent')
assert knowledge_agent['type'] == 'knowledge'
assert (
knowledge_agent['content']
== 'This is a test knowledge microagent for testing purposes.'
)
assert knowledge_agent['triggers'] == mock_knowledge_microagent.triggers
assert len(knowledge_agent['inputs']) == 1
assert knowledge_agent['inputs'][0]['name'] == 'topic'
assert knowledge_agent['tools'] == ['search', 'fetch']
assert 'created_at' in knowledge_agent
assert 'git_provider' in knowledge_agent
assert knowledge_agent['git_provider'] == 'github'
assert 'path' in knowledge_agent
assert (
knowledge_agent['path'] == '.openhands/microagents/test_knowledge_agent.md'
)
@pytest.mark.asyncio
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_authentication_error(
self, mock_provider_handler_class, test_client, mock_provider_tokens
):
"""Test authentication error when verifying repository."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_provider_handler.verify_repo_provider = AsyncMock(
side_effect=AuthenticationError('Invalid credentials')
)
mock_provider_handler_class.return_value = mock_provider_handler
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 401
assert response.json() == 'Invalid credentials'
@pytest.mark.asyncio
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_clone_failure(
self,
mock_provider_handler_class,
mock_subprocess_run,
test_client,
mock_provider_tokens,
):
"""Test error when git clone fails."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run to fail
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = 'Repository not found'
mock_subprocess_run.return_value = mock_result
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 500
assert 'Failed to clone repository' in response.json()
@pytest.mark.asyncio
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_no_microagents_directory(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_mkdtemp,
test_client,
mock_provider_tokens,
):
"""Test when repository has no .openhands/microagents directory."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Mock tempfile.mkdtemp to return a path that doesn't have microagents
temp_dir = '/tmp/test_no_microagents'
mock_mkdtemp.return_value = temp_dir
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert data == []
@pytest.mark.asyncio
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_empty_directory(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
test_client,
mock_provider_tokens,
):
"""Test when microagents directory exists but is empty."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Mock load_microagents_from_dir to return empty results
mock_load_microagents.return_value = ({}, {})
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert data == []
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_different_providers(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_repo_microagent,
):
"""Test microagents endpoint with GitHub provider."""
# Setup mocks
provider_tokens = MappingProxyType(
{
ProviderType.GITHUB: ProviderToken(
token=SecretStr('ghp_test_token'), host='github.com'
)
}
)
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create temporary directory with microagents
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create mock microagents with proper absolute paths
repo_agent_with_path = RepoMicroagent(
name='test_repo_agent',
content='This is a test repository microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_repo_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[
InputMetadata(
name='query',
type='str',
description='Search query for the repository',
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='git', command='git'),
MCPStdioServerConfig(name='file_editor', command='editor'),
]
),
),
source=str(
Path(temp_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'test_repo_agent.md'
),
type=MicroagentType.REPO_KNOWLEDGE,
)
mock_repo_agents = {'test_repo_agent': repo_agent_with_path}
mock_knowledge_agents = {}
mock_load_microagents.return_value = (mock_repo_agents, mock_knowledge_agents)
mock_mkdtemp.return_value = temp_dir
try:
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]['name'] == 'test_repo_agent'
assert data[0]['type'] == 'repo'
assert 'created_at' in data[0]
assert 'git_provider' in data[0]
assert data[0]['git_provider'] == 'github'
assert 'path' in data[0]
assert data[0]['path'] == '.openhands/microagents/test_repo_agent.md'
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_with_external_auth(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_provider_tokens,
mock_repo_microagent,
):
"""Test microagents endpoint with external authentication."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
test_client.app.dependency_overrides[get_access_token] = lambda: SecretStr(
'external_token'
)
test_client.app.dependency_overrides[get_user_id] = lambda: 'external_user'
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create temporary directory with microagents
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create mock microagents with proper absolute paths
repo_agent_with_path = RepoMicroagent(
name='test_repo_agent',
content='This is a test repository microagent for testing purposes.',
metadata=MicroagentMetadata(
name='test_repo_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[
InputMetadata(
name='query',
type='str',
description='Search query for the repository',
)
],
mcp_tools=MCPConfig(
stdio_servers=[
MCPStdioServerConfig(name='git', command='git'),
MCPStdioServerConfig(name='file_editor', command='editor'),
]
),
),
source=str(
Path(temp_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'test_repo_agent.md'
),
type=MicroagentType.REPO_KNOWLEDGE,
)
mock_repo_agents = {'test_repo_agent': repo_agent_with_path}
mock_knowledge_agents = {}
mock_load_microagents.return_value = (mock_repo_agents, mock_knowledge_agents)
mock_mkdtemp.return_value = temp_dir
try:
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]['name'] == 'test_repo_agent'
assert 'created_at' in data[0]
assert 'git_provider' in data[0]
assert data[0]['git_provider'] == 'github'
assert 'path' in data[0]
assert data[0]['path'] == '.openhands/microagents/test_repo_agent.md'
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.asyncio
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_generic_exception(
self, mock_provider_handler_class, test_client, mock_provider_tokens
):
"""Test handling of generic exceptions."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_provider_handler.verify_repo_provider = AsyncMock(
side_effect=Exception('Unexpected error')
)
mock_provider_handler_class.return_value = mock_provider_handler
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 500
assert 'Error scanning repository' in response.json()
@pytest.mark.asyncio
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_timeout(
self,
mock_provider_handler_class,
mock_subprocess_run,
test_client,
mock_provider_tokens,
):
"""Test timeout handling during git clone."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run to raise timeout
import subprocess
mock_subprocess_run.side_effect = subprocess.TimeoutExpired('git', 30)
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 500
assert 'Error scanning repository' in response.json()
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_microagents_without_mcp_tools(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_provider_tokens,
):
"""Test microagents without MCP tools."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create temporary directory with microagents
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create microagent without MCP tools
repo_microagent = RepoMicroagent(
name='simple_agent',
content='Simple agent without MCP tools',
metadata=MicroagentMetadata(
name='simple_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[],
mcp_tools=None,
),
source=str(
Path(temp_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'simple_agent.md'
),
type=MicroagentType.REPO_KNOWLEDGE,
)
# Mock load_microagents_from_dir
mock_repo_agents = {'simple_agent': repo_microagent}
mock_knowledge_agents = {}
mock_load_microagents.return_value = (mock_repo_agents, mock_knowledge_agents)
mock_mkdtemp.return_value = temp_dir
try:
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]['name'] == 'simple_agent'
assert data[0]['tools'] == []
assert 'created_at' in data[0]
assert 'git_provider' in data[0]
assert data[0]['git_provider'] == 'github'
assert 'path' in data[0]
assert data[0]['path'] == '.openhands/microagents/simple_agent.md'
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_path_field_variations(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_provider_tokens,
):
"""Test path field with different microagent file locations and structures."""
# Setup mocks
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: mock_provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/repo',
git_provider=ProviderType.GITHUB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://ghp_test_token@github.com/test/repo.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create temporary directory with microagents
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create microagents with different source paths
repo_microagent_deep = RepoMicroagent(
name='deep_agent',
content='Agent in nested directory',
metadata=MicroagentMetadata(
name='deep_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[],
mcp_tools=None,
),
source=str(
Path(temp_dir)
/ 'repo'
/ '.openhands'
/ 'microagents'
/ 'nested'
/ 'deep_agent.md'
),
type=MicroagentType.REPO_KNOWLEDGE,
)
knowledge_microagent_root = KnowledgeMicroagent(
name='root_agent',
content='Agent in root microagents directory',
metadata=MicroagentMetadata(
name='root_agent',
type=MicroagentType.KNOWLEDGE,
inputs=[],
mcp_tools=None,
),
source=str(
Path(temp_dir) / 'repo' / '.openhands' / 'microagents' / 'root_agent.md'
),
type=MicroagentType.KNOWLEDGE,
triggers=[],
)
# Mock load_microagents_from_dir
mock_repo_agents = {'deep_agent': repo_microagent_deep}
mock_knowledge_agents = {'root_agent': knowledge_microagent_root}
mock_load_microagents.return_value = (mock_repo_agents, mock_knowledge_agents)
mock_mkdtemp.return_value = temp_dir
try:
# Execute test
response = test_client.get('/api/user/repository/test/repo/microagents')
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 2
# Check repo microagent with nested path
repo_agent = next(m for m in data if m['name'] == 'deep_agent')
assert repo_agent['type'] == 'repo'
assert 'path' in repo_agent
assert repo_agent['path'] == '.openhands/microagents/nested/deep_agent.md'
# Check knowledge microagent with root path
knowledge_agent = next(m for m in data if m['name'] == 'root_agent')
assert knowledge_agent['type'] == 'knowledge'
assert 'path' in knowledge_agent
assert knowledge_agent['path'] == '.openhands/microagents/root_agent.md'
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.asyncio
@patch(
'openhands.server.routes.git._get_file_creation_time',
return_value=datetime.now(),
)
@patch('openhands.server.routes.git.tempfile.mkdtemp')
@patch('openhands.server.routes.git.load_microagents_from_dir')
@patch('openhands.server.routes.git.subprocess.run')
@patch('openhands.server.routes.git.ProviderHandler')
async def test_get_microagents_path_field_gitlab_structure(
self,
mock_provider_handler_class,
mock_subprocess_run,
mock_load_microagents,
mock_mkdtemp,
mock_get_file_creation_time,
test_client,
mock_provider_tokens,
):
"""Test path field with GitLab repository structure (openhands-config)."""
# Setup mocks with GitLab provider
provider_tokens = MappingProxyType(
{
ProviderType.GITLAB: ProviderToken(
token=SecretStr('glpat_test_token'), host='gitlab.com'
)
}
)
test_client.app.dependency_overrides[get_provider_tokens] = (
lambda: provider_tokens
)
mock_provider_handler = MagicMock()
mock_repository = Repository(
id='123456',
full_name='test/openhands-config',
git_provider=ProviderType.GITLAB,
is_public=True,
stargazers_count=100,
)
mock_provider_handler.verify_repo_provider = AsyncMock(
return_value=mock_repository
)
mock_provider_handler.get_authenticated_git_url = AsyncMock(
return_value='https://glpat_test_token@gitlab.com/test/openhands-config.git'
)
mock_provider_handler_class.return_value = mock_provider_handler
# Mock subprocess.run for successful clone
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stderr = ''
mock_subprocess_run.return_value = mock_result
# Create temporary directory with GitLab structure
temp_dir = tempfile.mkdtemp()
microagents_dir = Path(temp_dir) / 'repo' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create microagent for GitLab structure
repo_microagent = RepoMicroagent(
name='gitlab_agent',
content='Agent in GitLab repository',
metadata=MicroagentMetadata(
name='gitlab_agent',
type=MicroagentType.REPO_KNOWLEDGE,
inputs=[],
mcp_tools=None,
),
source=str(Path(temp_dir) / 'repo' / 'microagents' / 'gitlab_agent.md'),
type=MicroagentType.REPO_KNOWLEDGE,
)
# Mock load_microagents_from_dir
mock_repo_agents = {'gitlab_agent': repo_microagent}
mock_knowledge_agents = {}
mock_load_microagents.return_value = (mock_repo_agents, mock_knowledge_agents)
mock_mkdtemp.return_value = temp_dir
try:
# Execute test
response = test_client.get(
'/api/user/repository/test/openhands-config/microagents'
)
# Assertions
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]['name'] == 'gitlab_agent'
assert data[0]['type'] == 'repo'
assert 'path' in data[0]
assert data[0]['path'] == 'microagents/gitlab_agent.md'
assert 'git_provider' in data[0]
assert data[0]['git_provider'] == 'gitlab'
finally:
shutil.rmtree(temp_dir, ignore_errors=True)