feat(backend): exclude sub-conversations when searching for conversations (#11733)

This commit is contained in:
Hiep Le 2025-11-15 00:21:27 +07:00 committed by GitHub
parent 2841e35f24
commit 833aae1833
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 671 additions and 3 deletions

View File

@ -26,6 +26,7 @@ class AppConversationInfoService(ABC):
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
page_id: str | None = None,
limit: int = 100,
include_sub_conversations: bool = False,
) -> AppConversationInfoPage:
"""Search for sandboxed conversations."""

View File

@ -99,6 +99,12 @@ async def search_app_conversations(
lte=100,
),
] = 100,
include_sub_conversations: Annotated[
bool,
Query(
title='If True, include sub-conversations in the results. If False (default), exclude all sub-conversations.'
),
] = False,
app_conversation_service: AppConversationService = (
app_conversation_service_dependency
),
@ -114,6 +120,7 @@ async def search_app_conversations(
updated_at__lt=updated_at__lt,
page_id=page_id,
limit=limit,
include_sub_conversations=include_sub_conversations,
)
@ -193,7 +200,8 @@ async def stream_app_conversation_start(
user_context: UserContext = user_context_dependency,
) -> list[AppConversationStartTask]:
"""Start an app conversation start task and stream updates from it.
Leaves the connection open until either the conversation starts or there was an error"""
Leaves the connection open until either the conversation starts or there was an error
"""
response = StreamingResponse(
_stream_app_conversation_start(request, user_context),
media_type='application/json',

View File

@ -30,6 +30,7 @@ class AppConversationService(ABC):
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
page_id: str | None = None,
limit: int = 100,
include_sub_conversations: bool = False,
) -> AppConversationPage:
"""Search for sandboxed conversations."""

View File

@ -105,6 +105,7 @@ class LiveStatusAppConversationService(GitAppConversationService):
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
page_id: str | None = None,
limit: int = 20,
include_sub_conversations: bool = False,
) -> AppConversationPage:
"""Search for sandboxed conversations."""
page = await self.app_conversation_info_service.search_app_conversation_info(
@ -116,6 +117,7 @@ class LiveStatusAppConversationService(GitAppConversationService):
sort_order=sort_order,
page_id=page_id,
limit=limit,
include_sub_conversations=include_sub_conversations,
)
conversations: list[AppConversation] = await self._build_app_conversations(
page.items

View File

@ -111,10 +111,18 @@ class SQLAppConversationInfoService(AppConversationInfoService):
sort_order: AppConversationSortOrder = AppConversationSortOrder.CREATED_AT_DESC,
page_id: str | None = None,
limit: int = 100,
include_sub_conversations: bool = False,
) -> AppConversationInfoPage:
"""Search for sandboxed conversations without permission checks."""
query = await self._secure_select()
# Conditionally exclude sub-conversations based on the parameter
if not include_sub_conversations:
# Exclude sub-conversations (only include top-level conversations)
query = query.where(
StoredConversationMetadata.parent_conversation_id.is_(None)
)
query = self._apply_filters(
query=query,
title__contains=title__contains,

View File

@ -6,9 +6,10 @@ import os
import re
import uuid
from datetime import datetime, timedelta, timezone
from typing import Annotated
import base62
from fastapi import APIRouter, Depends, Request, status
from fastapi import APIRouter, Depends, Query, Request, status
from fastapi.responses import JSONResponse
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel, ConfigDict, Field
@ -309,6 +310,12 @@ async def search_conversations(
limit: int = 20,
selected_repository: str | None = None,
conversation_trigger: ConversationTrigger | None = None,
include_sub_conversations: Annotated[
bool,
Query(
title='If True, include sub-conversations in the results. If False (default), exclude all sub-conversations.'
),
] = False,
conversation_store: ConversationStore = Depends(get_conversation_store),
app_conversation_service: AppConversationService = app_conversation_service_dependency,
) -> ConversationInfoResultSet:
@ -343,6 +350,7 @@ async def search_conversations(
limit=limit,
# Apply age filter at the service level if possible
created_at__gte=age_filter_date,
include_sub_conversations=include_sub_conversations,
)
# Convert V1 conversations to ConversationInfo format
@ -1187,6 +1195,7 @@ async def _fetch_v1_conversations_safe(
app_conversation_service: App conversation service for V1
v1_page_id: Page ID for V1 pagination
limit: Maximum number of results
include_sub_conversations: If True, include sub-conversations in results
Returns:
Tuple of (v1_conversations, v1_next_page_id)

View File

@ -623,3 +623,383 @@ class TestSQLAppConversationInfoService:
created_at__gte=start_time, created_at__lt=end_time
)
assert count == 2
@pytest.mark.asyncio
async def test_search_excludes_sub_conversations_by_default(
self,
service: SQLAppConversationInfoService,
):
"""Test that search excludes sub-conversations by default."""
# Create a parent conversation
parent_id = uuid4()
parent_info = AppConversationInfo(
id=parent_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent',
title='Parent Conversation',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations
sub_info_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub1',
title='Sub Conversation 1',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
)
sub_info_2 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub2',
title='Sub Conversation 2',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
)
# Save all conversations
await service.save_app_conversation_info(parent_info)
await service.save_app_conversation_info(sub_info_1)
await service.save_app_conversation_info(sub_info_2)
# Search without include_sub_conversations (default False)
page = await service.search_app_conversation_info()
# Should only return the parent conversation
assert len(page.items) == 1
assert page.items[0].id == parent_id
assert page.items[0].title == 'Parent Conversation'
assert page.items[0].parent_conversation_id is None
@pytest.mark.asyncio
async def test_search_includes_sub_conversations_when_flag_true(
self,
service: SQLAppConversationInfoService,
):
"""Test that search includes sub-conversations when include_sub_conversations=True."""
# Create a parent conversation
parent_id = uuid4()
parent_info = AppConversationInfo(
id=parent_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent',
title='Parent Conversation',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations
sub_info_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub1',
title='Sub Conversation 1',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
)
sub_info_2 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub2',
title='Sub Conversation 2',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
)
# Save all conversations
await service.save_app_conversation_info(parent_info)
await service.save_app_conversation_info(sub_info_1)
await service.save_app_conversation_info(sub_info_2)
# Search with include_sub_conversations=True
page = await service.search_app_conversation_info(
include_sub_conversations=True
)
# Should return all conversations (1 parent + 2 sub-conversations)
assert len(page.items) == 3
# Verify all conversations are present
conversation_ids = {item.id for item in page.items}
assert parent_id in conversation_ids
assert sub_info_1.id in conversation_ids
assert sub_info_2.id in conversation_ids
# Verify parent conversation has no parent_conversation_id
parent_item = next(item for item in page.items if item.id == parent_id)
assert parent_item.parent_conversation_id is None
# Verify sub-conversations have parent_conversation_id set
sub_item_1 = next(item for item in page.items if item.id == sub_info_1.id)
assert sub_item_1.parent_conversation_id == parent_id
sub_item_2 = next(item for item in page.items if item.id == sub_info_2.id)
assert sub_item_2.parent_conversation_id == parent_id
@pytest.mark.asyncio
async def test_search_sub_conversations_with_filters(
self,
service: SQLAppConversationInfoService,
):
"""Test that include_sub_conversations works correctly with other filters."""
# Create a parent conversation
parent_id = uuid4()
parent_info = AppConversationInfo(
id=parent_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent',
title='Parent Conversation',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations with different titles
sub_info_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub1',
title='Sub Conversation Alpha',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
)
sub_info_2 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub2',
title='Sub Conversation Beta',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
)
# Save all conversations
await service.save_app_conversation_info(parent_info)
await service.save_app_conversation_info(sub_info_1)
await service.save_app_conversation_info(sub_info_2)
# Search with title filter and include_sub_conversations=False (default)
page = await service.search_app_conversation_info(title__contains='Alpha')
# Should only find parent if it matches, but parent doesn't have "Alpha"
# So should find nothing or only sub if we include them
assert len(page.items) == 0
# Search with title filter and include_sub_conversations=True
page = await service.search_app_conversation_info(
title__contains='Alpha', include_sub_conversations=True
)
# Should find the sub-conversation with "Alpha" in title
assert len(page.items) == 1
assert page.items[0].title == 'Sub Conversation Alpha'
assert page.items[0].parent_conversation_id == parent_id
# Search with title filter for "Parent" and include_sub_conversations=True
page = await service.search_app_conversation_info(
title__contains='Parent', include_sub_conversations=True
)
# Should find the parent conversation
assert len(page.items) == 1
assert page.items[0].title == 'Parent Conversation'
assert page.items[0].parent_conversation_id is None
@pytest.mark.asyncio
async def test_search_sub_conversations_with_date_filters(
self,
service: SQLAppConversationInfoService,
):
"""Test that include_sub_conversations works correctly with date filters."""
# Create a parent conversation
parent_id = uuid4()
parent_info = AppConversationInfo(
id=parent_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent',
title='Parent Conversation',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations at different times
sub_info_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub1',
title='Sub Conversation 1',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
)
sub_info_2 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub2',
title='Sub Conversation 2',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
)
# Save all conversations
await service.save_app_conversation_info(parent_info)
await service.save_app_conversation_info(sub_info_1)
await service.save_app_conversation_info(sub_info_2)
# Search with date filter and include_sub_conversations=False (default)
cutoff_time = datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc)
page = await service.search_app_conversation_info(created_at__gte=cutoff_time)
# Should only return parent if it matches the filter, but parent is at 12:00
assert len(page.items) == 0
# Search with date filter and include_sub_conversations=True
page = await service.search_app_conversation_info(
created_at__gte=cutoff_time, include_sub_conversations=True
)
# Should find sub-conversations created after cutoff (sub_info_2 at 14:00)
assert len(page.items) == 1
assert page.items[0].id == sub_info_2.id
assert page.items[0].parent_conversation_id == parent_id
@pytest.mark.asyncio
async def test_search_multiple_parents_with_sub_conversations(
self,
service: SQLAppConversationInfoService,
):
"""Test search with multiple parent conversations and their sub-conversations."""
# Create first parent conversation
parent1_id = uuid4()
parent1_info = AppConversationInfo(
id=parent1_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent1',
title='Parent 1',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create second parent conversation
parent2_id = uuid4()
parent2_info = AppConversationInfo(
id=parent2_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent2',
title='Parent 2',
created_at=datetime(2024, 1, 1, 13, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations for parent1
sub1_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub1_1',
title='Sub 1-1',
parent_conversation_id=parent1_id,
created_at=datetime(2024, 1, 1, 14, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 14, 30, 0, tzinfo=timezone.utc),
)
# Create sub-conversations for parent2
sub2_1 = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id='sandbox_sub2_1',
title='Sub 2-1',
parent_conversation_id=parent2_id,
created_at=datetime(2024, 1, 1, 15, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 15, 30, 0, tzinfo=timezone.utc),
)
# Save all conversations
await service.save_app_conversation_info(parent1_info)
await service.save_app_conversation_info(parent2_info)
await service.save_app_conversation_info(sub1_1)
await service.save_app_conversation_info(sub2_1)
# Search without include_sub_conversations (default False)
page = await service.search_app_conversation_info()
# Should only return the 2 parent conversations
assert len(page.items) == 2
conversation_ids = {item.id for item in page.items}
assert parent1_id in conversation_ids
assert parent2_id in conversation_ids
assert sub1_1.id not in conversation_ids
assert sub2_1.id not in conversation_ids
# Search with include_sub_conversations=True
page = await service.search_app_conversation_info(
include_sub_conversations=True
)
# Should return all 4 conversations (2 parents + 2 sub-conversations)
assert len(page.items) == 4
conversation_ids = {item.id for item in page.items}
assert parent1_id in conversation_ids
assert parent2_id in conversation_ids
assert sub1_1.id in conversation_ids
assert sub2_1.id in conversation_ids
@pytest.mark.asyncio
async def test_search_sub_conversations_with_pagination(
self,
service: SQLAppConversationInfoService,
):
"""Test that include_sub_conversations works correctly with pagination."""
# Create a parent conversation
parent_id = uuid4()
parent_info = AppConversationInfo(
id=parent_id,
created_by_user_id='test_user_123',
sandbox_id='sandbox_parent',
title='Parent Conversation',
created_at=datetime(2024, 1, 1, 12, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 12, 30, 0, tzinfo=timezone.utc),
)
# Create multiple sub-conversations
sub_conversations = []
for i in range(5):
sub_info = AppConversationInfo(
id=uuid4(),
created_by_user_id='test_user_123',
sandbox_id=f'sandbox_sub{i}',
title=f'Sub Conversation {i}',
parent_conversation_id=parent_id,
created_at=datetime(2024, 1, 1, 13 + i, 0, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 1, 13 + i, 30, 0, tzinfo=timezone.utc),
)
sub_conversations.append(sub_info)
await service.save_app_conversation_info(sub_info)
# Save parent
await service.save_app_conversation_info(parent_info)
# Search with include_sub_conversations=True and pagination
page1 = await service.search_app_conversation_info(
include_sub_conversations=True, limit=3
)
# Should return 3 items (1 parent + 2 sub-conversations)
assert len(page1.items) == 3
assert page1.next_page_id is not None
# Get next page
page2 = await service.search_app_conversation_info(
include_sub_conversations=True, limit=3, page_id=page1.next_page_id
)
# Should return remaining items
assert len(page2.items) == 3
assert page2.next_page_id is None
# Verify all conversations are present across pages
all_ids = {item.id for item in page1.items} | {item.id for item in page2.items}
assert parent_id in all_ids
for sub_info in sub_conversations:
assert sub_info.id in all_ids

View File

@ -13,6 +13,7 @@ from openhands.app_server.app_conversation.app_conversation_info_service import
from openhands.app_server.app_conversation.app_conversation_models import (
AgentType,
AppConversationInfo,
AppConversationPage,
AppConversationStartRequest,
AppConversationStartTask,
AppConversationStartTaskStatus,
@ -22,6 +23,9 @@ from openhands.app_server.app_conversation.app_conversation_service import (
)
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.types import MicroagentMetadata, MicroagentType
from openhands.server.data_models.conversation_info_result_set import (
ConversationInfoResultSet,
)
from openhands.server.routes.conversation import (
AddMessageRequest,
add_message,
@ -29,11 +33,15 @@ from openhands.server.routes.conversation import (
)
from openhands.server.routes.manage_conversations import (
UpdateConversationRequest,
search_conversations,
update_conversation,
)
from openhands.server.session.conversation import ServerConversation
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
from openhands.storage.data_models.conversation_metadata import (
ConversationMetadata,
ConversationTrigger,
)
@pytest.mark.asyncio
@ -1200,3 +1208,254 @@ async def test_create_sub_conversation_with_planning_agent():
assert task.request.parent_conversation_id == parent_conversation_id
assert task.sandbox_id == sandbox_id
break
@pytest.mark.asyncio
async def test_search_conversations_include_sub_conversations_default_false():
"""Test that include_sub_conversations defaults to False when not provided."""
with patch('openhands.server.routes.manage_conversations.config') as mock_config:
mock_config.conversation_max_age_seconds = 864000 # 10 days
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
async def mock_get_running_agent_loops(*args, **kwargs):
return set()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
with patch(
'openhands.server.routes.manage_conversations.datetime'
) as mock_datetime:
mock_datetime.now.return_value = datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
)
mock_datetime.fromisoformat = datetime.fromisoformat
mock_datetime.timezone = timezone
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(results=[])
)
# Create a mock app conversation service
mock_app_conversation_service = AsyncMock()
mock_app_conversation_service.search_app_conversations.return_value = (
AppConversationPage(items=[])
)
# Call search_conversations without include_sub_conversations parameter
await search_conversations(
page_id=None,
limit=20,
selected_repository=None,
conversation_trigger=None,
conversation_store=mock_store,
app_conversation_service=mock_app_conversation_service,
)
# Verify that search_app_conversations was called with include_sub_conversations=False (default)
mock_app_conversation_service.search_app_conversations.assert_called_once()
call_kwargs = (
mock_app_conversation_service.search_app_conversations.call_args[1]
)
assert call_kwargs.get('include_sub_conversations') is False
@pytest.mark.asyncio
async def test_search_conversations_include_sub_conversations_explicit_false():
"""Test that include_sub_conversations=False is properly passed through."""
with patch('openhands.server.routes.manage_conversations.config') as mock_config:
mock_config.conversation_max_age_seconds = 864000 # 10 days
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
async def mock_get_running_agent_loops(*args, **kwargs):
return set()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
with patch(
'openhands.server.routes.manage_conversations.datetime'
) as mock_datetime:
mock_datetime.now.return_value = datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
)
mock_datetime.fromisoformat = datetime.fromisoformat
mock_datetime.timezone = timezone
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(results=[])
)
# Create a mock app conversation service
mock_app_conversation_service = AsyncMock()
mock_app_conversation_service.search_app_conversations.return_value = (
AppConversationPage(items=[])
)
# Call search_conversations with include_sub_conversations=False
await search_conversations(
page_id=None,
limit=20,
selected_repository=None,
conversation_trigger=None,
include_sub_conversations=False,
conversation_store=mock_store,
app_conversation_service=mock_app_conversation_service,
)
# Verify that search_app_conversations was called with include_sub_conversations=False
mock_app_conversation_service.search_app_conversations.assert_called_once()
call_kwargs = (
mock_app_conversation_service.search_app_conversations.call_args[1]
)
assert call_kwargs.get('include_sub_conversations') is False
@pytest.mark.asyncio
async def test_search_conversations_include_sub_conversations_explicit_true():
"""Test that include_sub_conversations=True is properly passed through."""
with patch('openhands.server.routes.manage_conversations.config') as mock_config:
mock_config.conversation_max_age_seconds = 864000 # 10 days
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
async def mock_get_running_agent_loops(*args, **kwargs):
return set()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
with patch(
'openhands.server.routes.manage_conversations.datetime'
) as mock_datetime:
mock_datetime.now.return_value = datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
)
mock_datetime.fromisoformat = datetime.fromisoformat
mock_datetime.timezone = timezone
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(results=[])
)
# Create a mock app conversation service
mock_app_conversation_service = AsyncMock()
mock_app_conversation_service.search_app_conversations.return_value = (
AppConversationPage(items=[])
)
# Call search_conversations with include_sub_conversations=True
await search_conversations(
page_id=None,
limit=20,
selected_repository=None,
conversation_trigger=None,
include_sub_conversations=True,
conversation_store=mock_store,
app_conversation_service=mock_app_conversation_service,
)
# Verify that search_app_conversations was called with include_sub_conversations=True
mock_app_conversation_service.search_app_conversations.assert_called_once()
call_kwargs = (
mock_app_conversation_service.search_app_conversations.call_args[1]
)
assert call_kwargs.get('include_sub_conversations') is True
@pytest.mark.asyncio
async def test_search_conversations_include_sub_conversations_with_other_filters():
"""Test that include_sub_conversations works correctly with other filters."""
with patch('openhands.server.routes.manage_conversations.config') as mock_config:
mock_config.conversation_max_age_seconds = 864000 # 10 days
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
async def mock_get_running_agent_loops(*args, **kwargs):
return set()
async def mock_get_connections(*args, **kwargs):
return {}
async def get_agent_loop_info(*args, **kwargs):
return []
mock_manager.get_running_agent_loops = mock_get_running_agent_loops
mock_manager.get_connections = mock_get_connections
mock_manager.get_agent_loop_info = get_agent_loop_info
with patch(
'openhands.server.routes.manage_conversations.datetime'
) as mock_datetime:
mock_datetime.now.return_value = datetime.fromisoformat(
'2025-01-01T00:00:00+00:00'
)
mock_datetime.fromisoformat = datetime.fromisoformat
mock_datetime.timezone = timezone
# Mock the conversation store
mock_store = MagicMock()
mock_store.search = AsyncMock(
return_value=ConversationInfoResultSet(results=[])
)
# Create a mock app conversation service
mock_app_conversation_service = AsyncMock()
mock_app_conversation_service.search_app_conversations.return_value = (
AppConversationPage(items=[])
)
# Create a valid base64-encoded page_id for testing
import base64
page_id_data = json.dumps({'v0': None, 'v1': 'test_v1_page_id'})
encoded_page_id = base64.b64encode(page_id_data.encode()).decode()
# Call search_conversations with include_sub_conversations and other filters
await search_conversations(
page_id=encoded_page_id,
limit=50,
selected_repository='test/repo',
conversation_trigger=ConversationTrigger.GUI,
include_sub_conversations=True,
conversation_store=mock_store,
app_conversation_service=mock_app_conversation_service,
)
# Verify that search_app_conversations was called with all parameters including include_sub_conversations=True
mock_app_conversation_service.search_app_conversations.assert_called_once()
call_kwargs = (
mock_app_conversation_service.search_app_conversations.call_args[1]
)
assert call_kwargs.get('include_sub_conversations') is True
assert call_kwargs.get('page_id') == 'test_v1_page_id'
assert call_kwargs.get('limit') == 50