refactor(backend): update get_microagent_management_conversations API to support V1 (#11313)

Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Hiep Le
2025-11-04 17:44:44 +07:00
committed by GitHub
parent 2fc8ab2601
commit fa431fb956
2 changed files with 421 additions and 113 deletions

View File

@@ -1104,47 +1104,154 @@ def add_experiment_config_for_conversation(
return False return False
@app.get('/microagent-management/conversations') def _parse_combined_page_id(page_id: str | None) -> tuple[str | None, str | None]:
async def get_microagent_management_conversations( """Parse combined page_id to extract separate V0 and V1 page_ids.
selected_repository: str,
page_id: str | None = None,
limit: int = 20,
conversation_store: ConversationStore = Depends(get_conversation_store),
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
) -> ConversationInfoResultSet:
"""Get conversations for the microagent management page with pagination support.
This endpoint returns conversations with conversation_trigger = 'microagent_management'
and only includes conversations with active PRs. Pagination is supported.
Args: Args:
page_id: Optional page ID for pagination page_id: Combined page_id (base64-encoded JSON) or legacy V0 page_id
limit: Maximum number of results per page (default: 20)
selected_repository: Optional repository filter to limit results to a specific repository
conversation_store: Conversation store dependency
provider_tokens: Provider tokens for checking PR status
"""
conversation_metadata_result_set = await conversation_store.search(page_id, limit)
# Apply age filter first using common function Returns:
filtered_results = _filter_conversations_by_age( Tuple of (v0_page_id, v1_page_id)
conversation_metadata_result_set.results, config.conversation_max_age_seconds """
v0_page_id = None
v1_page_id = None
if page_id:
try:
# Try to parse as JSON first
page_data = json.loads(base64.b64decode(page_id))
v0_page_id = page_data.get('v0')
v1_page_id = page_data.get('v1')
except (json.JSONDecodeError, TypeError, Exception):
# Fallback: treat as v0 page_id for backward compatibility
# This catches base64 decode errors and any other parsing issues
v0_page_id = page_id
return v0_page_id, v1_page_id
async def _fetch_v1_conversations_safe(
app_conversation_service: AppConversationService,
v1_page_id: str | None,
limit: int,
) -> tuple[list[ConversationInfo], str | None]:
"""Safely fetch V1 conversations with error handling.
Args:
app_conversation_service: App conversation service for V1
v1_page_id: Page ID for V1 pagination
limit: Maximum number of results
Returns:
Tuple of (v1_conversations, v1_next_page_id)
"""
v1_conversations = []
v1_next_page_id = None
try:
age_filter_date = None
if config.conversation_max_age_seconds:
age_filter_date = datetime.now(timezone.utc) - timedelta(
seconds=config.conversation_max_age_seconds
)
app_conversation_page = await app_conversation_service.search_app_conversations(
page_id=v1_page_id,
limit=limit,
created_at__gte=age_filter_date,
)
v1_conversations = [
_to_conversation_info(app_conv) for app_conv in app_conversation_page.items
]
v1_next_page_id = app_conversation_page.next_page_id
except Exception as e:
# V1 system might not be available or initialized yet
logger.debug(f'V1 conversation service not available: {str(e)}')
return v1_conversations, v1_next_page_id
async def _process_v0_conversations(
conversation_metadata_result_set,
) -> list[ConversationInfo]:
"""Process V0 conversations with age filtering and agent loop info.
Args:
conversation_metadata_result_set: Result set from V0 conversation store
Returns:
List of processed ConversationInfo objects
"""
# Apply age filter to V0 conversations
v0_filtered_results = _filter_conversations_by_age(
conversation_metadata_result_set.results,
config.conversation_max_age_seconds,
) )
# Check if the last PR is active (not closed/merged) v0_conversation_ids = set(
provider_handler = ProviderHandler(provider_tokens) conversation.conversation_id for conversation in v0_filtered_results
)
# Apply additional filters # Get agent loop info for V0 conversations
final_filtered_results = [] await conversation_manager.get_connections(filter_to_sids=v0_conversation_ids)
for conversation in filtered_results: v0_agent_loop_info = await conversation_manager.get_agent_loop_info(
filter_to_sids=v0_conversation_ids
)
v0_agent_loop_info_by_conversation_id = {
info.conversation_id: info for info in v0_agent_loop_info
}
# Convert to ConversationInfo objects
v0_conversations = await wait_all(
_get_conversation_info(
conversation=conversation,
num_connections=sum(
1
for conversation_id in v0_agent_loop_info_by_conversation_id.values()
if conversation_id == conversation.conversation_id
),
agent_loop_info=v0_agent_loop_info_by_conversation_id.get(
conversation.conversation_id
),
)
for conversation in v0_filtered_results
)
return v0_conversations
async def _apply_microagent_filters(
conversations: list[ConversationInfo],
selected_repository: str,
provider_handler: ProviderHandler,
) -> list[ConversationInfo]:
"""Apply microagent management specific filters to conversations.
Filters conversations by:
- Trigger type (MICROAGENT_MANAGEMENT)
- Repository match
- PR status (only open PRs)
Args:
conversations: List of conversations to filter
selected_repository: Repository to filter by
provider_handler: Handler for checking PR status
Returns:
Filtered list of conversations
"""
filtered = []
for conversation in conversations:
# Only include microagent_management conversations # Only include microagent_management conversations
if conversation.trigger != ConversationTrigger.MICROAGENT_MANAGEMENT: if conversation.trigger != ConversationTrigger.MICROAGENT_MANAGEMENT:
continue continue
# Apply repository filter if specified # Apply repository filter
if conversation.selected_repository != selected_repository: if conversation.selected_repository != selected_repository:
continue continue
# Check if PR is still open
if ( if (
conversation.pr_number conversation.pr_number
and len(conversation.pr_number) > 0 and len(conversation.pr_number) > 0
@@ -1159,12 +1266,101 @@ async def get_microagent_management_conversations(
# Skip this conversation if the PR is closed/merged # Skip this conversation if the PR is closed/merged
continue continue
final_filtered_results.append(conversation) filtered.append(conversation)
return await _build_conversation_result_set( return filtered
final_filtered_results, conversation_metadata_result_set.next_page_id
def _create_combined_page_id(
v0_next_page_id: str | None, v1_next_page_id: str | None
) -> str | None:
"""Create a combined page_id from V0 and V1 page_ids.
Args:
v0_next_page_id: Next page ID for V0 conversations
v1_next_page_id: Next page ID for V1 conversations
Returns:
Base64-encoded JSON combining both page_ids, or None if no next pages
"""
if not v0_next_page_id and not v1_next_page_id:
return None
next_page_data = {
'v0': v0_next_page_id,
'v1': v1_next_page_id,
}
return base64.b64encode(json.dumps(next_page_data).encode()).decode()
@app.get('/microagent-management/conversations')
async def get_microagent_management_conversations(
selected_repository: str,
page_id: str | None = None,
limit: int = 20,
conversation_store: ConversationStore = Depends(get_conversation_store),
provider_tokens: PROVIDER_TOKEN_TYPE = Depends(get_provider_tokens),
app_conversation_service: AppConversationService = app_conversation_service_dependency,
) -> ConversationInfoResultSet:
"""Get conversations for the microagent management page with pagination support.
This endpoint returns conversations with conversation_trigger = 'microagent_management'
and only includes conversations with active PRs. Pagination is supported.
Args:
page_id: Optional page ID for pagination
limit: Maximum number of results per page (default: 20)
selected_repository: Repository filter to limit results to a specific repository
conversation_store: Conversation store dependency
provider_tokens: Provider tokens for checking PR status
app_conversation_service: App conversation service for V1 conversations
Returns:
ConversationInfoResultSet with filtered and paginated results
"""
# Parse page_id to extract V0 and V1 components
v0_page_id, v1_page_id = _parse_combined_page_id(page_id)
# Fetch V0 conversations
conversation_metadata_result_set = await conversation_store.search(
v0_page_id, limit
) )
# Fetch V1 conversations (with graceful error handling)
v1_conversations, v1_next_page_id = await _fetch_v1_conversations_safe(
app_conversation_service, v1_page_id, limit
)
# Process V0 conversations
v0_conversations = await _process_v0_conversations(conversation_metadata_result_set)
# Apply microagent-specific filters
provider_handler = ProviderHandler(provider_tokens)
v0_filtered = await _apply_microagent_filters(
v0_conversations, selected_repository, provider_handler
)
v1_filtered = await _apply_microagent_filters(
v1_conversations, selected_repository, provider_handler
)
# Combine and sort results
all_conversations = v0_filtered + v1_filtered
all_conversations.sort(
key=lambda x: x.created_at or datetime.min.replace(tzinfo=timezone.utc),
reverse=True,
)
# Limit to requested number of results
final_results = all_conversations[:limit]
# Create combined page_id for pagination
next_page_id = _create_combined_page_id(
conversation_metadata_result_set.next_page_id, v1_next_page_id
)
return ConversationInfoResultSet(results=final_results, next_page_id=next_page_id)
def _to_conversation_info(app_conversation: AppConversation) -> ConversationInfo: def _to_conversation_info(app_conversation: AppConversation) -> ConversationInfo:
"""Convert a V1 AppConversation into an old style ConversationInfo""" """Convert a V1 AppConversation into an old style ConversationInfo"""

View File

@@ -1,8 +1,13 @@
import base64
import json
from datetime import datetime, timezone from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from openhands.app_server.app_conversation.app_conversation_service import (
AppConversationService,
)
from openhands.integrations.provider import ProviderHandler from openhands.integrations.provider import ProviderHandler
from openhands.server.data_models.conversation_info_result_set import ( from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet, ConversationInfoResultSet,
@@ -17,6 +22,54 @@ from openhands.storage.data_models.conversation_metadata import (
) )
def _create_mock_app_conversation_service():
"""Create a mock AppConversationService that returns empty V1 results."""
mock_service = MagicMock(spec=AppConversationService)
mock_service.search_app_conversations = AsyncMock(
return_value=MagicMock(items=[], next_page_id=None)
)
return mock_service
def _decode_combined_page_id(page_id: str | None) -> dict:
"""Decode a combined page_id to get v0 and v1 components."""
if not page_id:
return {'v0': None, 'v1': None}
try:
return json.loads(base64.b64decode(page_id))
except Exception:
# Legacy format - just v0
return {'v0': page_id, 'v1': None}
async def _mock_wait_all(coros):
"""Mock implementation of wait_all that properly awaits coroutines."""
results = []
for coro in coros:
if hasattr(coro, '__await__'):
results.append(await coro)
else:
results.append(coro)
return results
def _setup_common_mocks():
"""Set up common mocks used by all tests."""
return {
'config': patch('openhands.server.routes.manage_conversations.config'),
'conversation_manager': patch(
'openhands.server.routes.manage_conversations.conversation_manager'
),
'wait_all': patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
'provider_handler': patch(
'openhands.server.routes.manage_conversations.ProviderHandler'
),
}
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_microagent_management_conversations_success(): async def test_get_microagent_management_conversations_success():
"""Test successful retrieval of microagent management conversations.""" """Test successful retrieval of microagent management conversations."""
@@ -64,24 +117,30 @@ async def test_get_microagent_management_conversations_success():
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_provider_handler.is_pr_open = AsyncMock(return_value=True) mock_provider_handler.is_pr_open = AsyncMock(return_value=True)
# Mock app conversation service
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function
mock_build_result.return_value = ConversationInfoResultSet(
results=[], next_page_id='next_page_456'
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 # 24 hours mock_config.conversation_max_age_seconds = 86400 # 24 hours
# Mock conversation manager
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function with correct parameter order # Call the function with correct parameter order
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository=selected_repository, selected_repository=selected_repository,
@@ -89,11 +148,16 @@ async def test_get_microagent_management_conversations_success():
limit=limit, limit=limit,
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify the result # Verify the result
assert isinstance(result, ConversationInfoResultSet) assert isinstance(result, ConversationInfoResultSet)
assert result.next_page_id == 'next_page_456'
# Decode the combined page_id to verify v0 component
decoded_page_id = _decode_combined_page_id(result.next_page_id)
assert decoded_page_id['v0'] == 'next_page_456'
assert decoded_page_id['v1'] is None
# Verify conversation store was called correctly # Verify conversation store was called correctly
mock_conversation_store.search.assert_called_once_with(page_id, limit) mock_conversation_store.search.assert_called_once_with(page_id, limit)
@@ -114,26 +178,31 @@ async def test_get_microagent_management_conversations_no_results():
# Mock provider tokens # Mock provider tokens
mock_provider_tokens = {'github': 'token_123'} mock_provider_tokens = {'github': 'token_123'}
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch('openhands.server.routes.manage_conversations.ProviderHandler'), patch('openhands.server.routes.manage_conversations.ProviderHandler'),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function
mock_build_result.return_value = ConversationInfoResultSet(
results=[], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function with required selected_repository parameter # Call the function with required selected_repository parameter
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify the result # Verify the result
@@ -184,29 +253,34 @@ async def test_get_microagent_management_conversations_filter_by_repository():
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_provider_handler.is_pr_open = AsyncMock(return_value=True) mock_provider_handler.is_pr_open = AsyncMock(return_value=True)
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function - only repo1 should be included
mock_build_result.return_value = ConversationInfoResultSet(
results=[mock_conversations[0]], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function with repository filter # Call the function with repository filter
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo1', selected_repository='owner/repo1',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify only conversations from the specified repository are returned # Verify only conversations from the specified repository are returned
@@ -257,29 +331,34 @@ async def test_get_microagent_management_conversations_filter_by_trigger():
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_provider_handler.is_pr_open = AsyncMock(return_value=True) mock_provider_handler.is_pr_open = AsyncMock(return_value=True)
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function - only microagent_management should be included
mock_build_result.return_value = ConversationInfoResultSet(
results=[mock_conversations[0]], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function # Call the function
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify only microagent_management conversations are returned # Verify only microagent_management conversations are returned
@@ -330,29 +409,34 @@ async def test_get_microagent_management_conversations_filter_inactive_pr():
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_provider_handler.is_pr_open = AsyncMock(side_effect=[True, False]) mock_provider_handler.is_pr_open = AsyncMock(side_effect=[True, False])
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function - only active PR should be included
mock_build_result.return_value = ConversationInfoResultSet(
results=[mock_conversations[0]], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function # Call the function
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify only conversations with active PRs are returned # Verify only conversations with active PRs are returned
@@ -393,29 +477,34 @@ async def test_get_microagent_management_conversations_no_pr_number():
# Mock provider handler # Mock provider handler
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function
mock_build_result.return_value = ConversationInfoResultSet(
results=mock_conversations, next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function # Call the function
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify conversation without PR number is included # Verify conversation without PR number is included
@@ -456,29 +545,34 @@ async def test_get_microagent_management_conversations_no_repository():
# Mock provider handler # Mock provider handler
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function - conversation should be filtered out due to repository mismatch
mock_build_result.return_value = ConversationInfoResultSet(
results=[], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function # Call the function
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify conversation without repository is filtered out # Verify conversation without repository is filtered out
@@ -532,29 +626,34 @@ async def test_get_microagent_management_conversations_age_filter():
mock_provider_handler = MagicMock(spec=ProviderHandler) mock_provider_handler = MagicMock(spec=ProviderHandler)
mock_provider_handler.is_pr_open = AsyncMock(return_value=True) mock_provider_handler.is_pr_open = AsyncMock(return_value=True)
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch( patch(
'openhands.server.routes.manage_conversations.ProviderHandler', 'openhands.server.routes.manage_conversations.ProviderHandler',
return_value=mock_provider_handler, return_value=mock_provider_handler,
), ),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function - only recent conversation should be included
mock_build_result.return_value = ConversationInfoResultSet(
results=[recent_conversation], next_page_id=None
)
# Mock config with short max age # Mock config with short max age
mock_config.conversation_max_age_seconds = 3600 # 1 hour mock_config.conversation_max_age_seconds = 3600 # 1 hour
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function # Call the function
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify only recent conversation is returned # Verify only recent conversation is returned
@@ -574,21 +673,25 @@ async def test_get_microagent_management_conversations_pagination():
# Mock provider tokens # Mock provider tokens
mock_provider_tokens = {'github': 'token_123'} mock_provider_tokens = {'github': 'token_123'}
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch('openhands.server.routes.manage_conversations.ProviderHandler'), patch('openhands.server.routes.manage_conversations.ProviderHandler'),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function
mock_build_result.return_value = ConversationInfoResultSet(
results=[], next_page_id='next_page_789'
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function with pagination parameters # Call the function with pagination parameters
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
@@ -596,11 +699,15 @@ async def test_get_microagent_management_conversations_pagination():
limit=5, limit=5,
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify pagination parameters were passed correctly # Verify pagination parameters were passed correctly
mock_conversation_store.search.assert_called_once_with('test_page', 5) mock_conversation_store.search.assert_called_once_with('test_page', 5)
assert result.next_page_id == 'next_page_789'
# Decode and verify the next_page_id
decoded_page_id = _decode_combined_page_id(result.next_page_id)
assert decoded_page_id['v0'] == 'next_page_789'
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -615,26 +722,31 @@ async def test_get_microagent_management_conversations_default_parameters():
# Mock provider tokens # Mock provider tokens
mock_provider_tokens = {'github': 'token_123'} mock_provider_tokens = {'github': 'token_123'}
mock_app_conversation_service = _create_mock_app_conversation_service()
with ( with (
patch('openhands.server.routes.manage_conversations.ProviderHandler'), patch('openhands.server.routes.manage_conversations.ProviderHandler'),
patch(
'openhands.server.routes.manage_conversations._build_conversation_result_set'
) as mock_build_result,
patch('openhands.server.routes.manage_conversations.config') as mock_config, patch('openhands.server.routes.manage_conversations.config') as mock_config,
patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_conv_mgr,
patch(
'openhands.server.routes.manage_conversations.wait_all',
side_effect=_mock_wait_all,
),
): ):
# Mock the build result function
mock_build_result.return_value = ConversationInfoResultSet(
results=[], next_page_id=None
)
# Mock config # Mock config
mock_config.conversation_max_age_seconds = 86400 mock_config.conversation_max_age_seconds = 86400
mock_conv_mgr.get_connections = AsyncMock(return_value=[])
mock_conv_mgr.get_agent_loop_info = AsyncMock(return_value=[])
# Call the function without parameters (selected_repository is required) # Call the function without parameters (selected_repository is required)
result = await get_microagent_management_conversations( result = await get_microagent_management_conversations(
selected_repository='owner/repo', selected_repository='owner/repo',
conversation_store=mock_conversation_store, conversation_store=mock_conversation_store,
provider_tokens=mock_provider_tokens, provider_tokens=mock_provider_tokens,
app_conversation_service=mock_app_conversation_service,
) )
# Verify default values were used # Verify default values were used