mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
Add sandbox_id__eq filter to AppConversationService search and count methods (#13387)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -12,21 +12,28 @@ from fastapi import HTTPException, status
|
||||
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversation,
|
||||
AppConversationPage,
|
||||
)
|
||||
from openhands.app_server.app_conversation.app_conversation_router import (
|
||||
batch_get_app_conversations,
|
||||
count_app_conversations,
|
||||
search_app_conversations,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
|
||||
|
||||
def _make_mock_app_conversation(conversation_id=None, user_id='test-user'):
|
||||
def _make_mock_app_conversation(
|
||||
conversation_id=None, user_id='test-user', sandbox_id=None
|
||||
):
|
||||
"""Create a mock AppConversation for testing."""
|
||||
if conversation_id is None:
|
||||
conversation_id = uuid4()
|
||||
if sandbox_id is None:
|
||||
sandbox_id = str(uuid4())
|
||||
return AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id=user_id,
|
||||
sandbox_id=str(uuid4()),
|
||||
sandbox_id=sandbox_id,
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
)
|
||||
|
||||
@@ -34,11 +41,17 @@ def _make_mock_app_conversation(conversation_id=None, user_id='test-user'):
|
||||
def _make_mock_service(
|
||||
get_conversation_return=None,
|
||||
batch_get_return=None,
|
||||
search_return=None,
|
||||
count_return=0,
|
||||
):
|
||||
"""Create a mock AppConversationService for testing."""
|
||||
service = MagicMock()
|
||||
service.get_app_conversation = AsyncMock(return_value=get_conversation_return)
|
||||
service.batch_get_app_conversations = AsyncMock(return_value=batch_get_return or [])
|
||||
service.search_app_conversations = AsyncMock(
|
||||
return_value=search_return or AppConversationPage(items=[])
|
||||
)
|
||||
service.count_app_conversations = AsyncMock(return_value=count_return)
|
||||
return service
|
||||
|
||||
|
||||
@@ -207,3 +220,157 @@ class TestBatchGetAppConversations:
|
||||
assert result[0] is not None
|
||||
assert result[0].id == uuid1
|
||||
assert result[1] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSearchAppConversations:
|
||||
"""Test suite for search_app_conversations endpoint."""
|
||||
|
||||
async def test_search_with_sandbox_id_filter(self):
|
||||
"""Test that sandbox_id__eq filter is passed to the service.
|
||||
|
||||
Arrange: Create mock service and specific sandbox_id
|
||||
Act: Call search_app_conversations with sandbox_id__eq
|
||||
Assert: Service is called with the sandbox_id__eq parameter
|
||||
"""
|
||||
# Arrange
|
||||
sandbox_id = 'test-sandbox-123'
|
||||
mock_conversation = _make_mock_app_conversation(sandbox_id=sandbox_id)
|
||||
mock_service = _make_mock_service(
|
||||
search_return=AppConversationPage(items=[mock_conversation])
|
||||
)
|
||||
|
||||
# Act
|
||||
result = await search_app_conversations(
|
||||
sandbox_id__eq=sandbox_id,
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.search_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0].sandbox_id == sandbox_id
|
||||
|
||||
async def test_search_without_sandbox_id_filter(self):
|
||||
"""Test that sandbox_id__eq defaults to None when not provided.
|
||||
|
||||
Arrange: Create mock service
|
||||
Act: Call search_app_conversations without sandbox_id__eq
|
||||
Assert: Service is called with sandbox_id__eq=None
|
||||
"""
|
||||
# Arrange
|
||||
mock_service = _make_mock_service()
|
||||
|
||||
# Act
|
||||
await search_app_conversations(
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.search_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') is None
|
||||
|
||||
async def test_search_with_sandbox_id_and_other_filters(self):
|
||||
"""Test that sandbox_id__eq works correctly with other filters.
|
||||
|
||||
Arrange: Create mock service
|
||||
Act: Call search_app_conversations with sandbox_id__eq and other filters
|
||||
Assert: Service is called with all parameters correctly
|
||||
"""
|
||||
# Arrange
|
||||
sandbox_id = 'test-sandbox-456'
|
||||
mock_service = _make_mock_service()
|
||||
|
||||
# Act
|
||||
await search_app_conversations(
|
||||
title__contains='test',
|
||||
sandbox_id__eq=sandbox_id,
|
||||
limit=50,
|
||||
include_sub_conversations=True,
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.search_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.search_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id
|
||||
assert call_kwargs.get('title__contains') == 'test'
|
||||
assert call_kwargs.get('limit') == 50
|
||||
assert call_kwargs.get('include_sub_conversations') is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestCountAppConversations:
|
||||
"""Test suite for count_app_conversations endpoint."""
|
||||
|
||||
async def test_count_with_sandbox_id_filter(self):
|
||||
"""Test that sandbox_id__eq filter is passed to the service.
|
||||
|
||||
Arrange: Create mock service with count return value
|
||||
Act: Call count_app_conversations with sandbox_id__eq
|
||||
Assert: Service is called with the sandbox_id__eq parameter
|
||||
"""
|
||||
# Arrange
|
||||
sandbox_id = 'test-sandbox-789'
|
||||
mock_service = _make_mock_service(count_return=5)
|
||||
|
||||
# Act
|
||||
result = await count_app_conversations(
|
||||
sandbox_id__eq=sandbox_id,
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.count_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.count_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id
|
||||
assert result == 5
|
||||
|
||||
async def test_count_without_sandbox_id_filter(self):
|
||||
"""Test that sandbox_id__eq defaults to None when not provided.
|
||||
|
||||
Arrange: Create mock service
|
||||
Act: Call count_app_conversations without sandbox_id__eq
|
||||
Assert: Service is called with sandbox_id__eq=None
|
||||
"""
|
||||
# Arrange
|
||||
mock_service = _make_mock_service(count_return=10)
|
||||
|
||||
# Act
|
||||
result = await count_app_conversations(
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.count_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.count_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') is None
|
||||
assert result == 10
|
||||
|
||||
async def test_count_with_sandbox_id_and_other_filters(self):
|
||||
"""Test that sandbox_id__eq works correctly with other filters.
|
||||
|
||||
Arrange: Create mock service
|
||||
Act: Call count_app_conversations with sandbox_id__eq and other filters
|
||||
Assert: Service is called with all parameters correctly
|
||||
"""
|
||||
# Arrange
|
||||
sandbox_id = 'test-sandbox-abc'
|
||||
mock_service = _make_mock_service(count_return=3)
|
||||
|
||||
# Act
|
||||
result = await count_app_conversations(
|
||||
title__contains='test',
|
||||
sandbox_id__eq=sandbox_id,
|
||||
app_conversation_service=mock_service,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_service.count_app_conversations.assert_called_once()
|
||||
call_kwargs = mock_service.count_app_conversations.call_args[1]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id
|
||||
assert call_kwargs.get('title__contains') == 'test'
|
||||
assert result == 3
|
||||
|
||||
@@ -2097,6 +2097,120 @@ class TestLiveStatusAppConversationService:
|
||||
|
||||
assert captured['workspace_working_dir'] == '/workspace/project'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_app_conversations_with_sandbox_id_filter(self):
|
||||
"""Test that search_app_conversations passes sandbox_id__eq to the info service.
|
||||
|
||||
This verifies that the sandbox_id filter is correctly propagated through
|
||||
the service layer to the underlying info service.
|
||||
"""
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationInfoPage,
|
||||
)
|
||||
|
||||
# Create test data with different sandbox IDs
|
||||
sandbox_id_alpha = 'sandbox-alpha-123'
|
||||
sandbox_id_beta = 'sandbox-beta-456'
|
||||
|
||||
conv_alpha = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id=None,
|
||||
sandbox_id=sandbox_id_alpha,
|
||||
title='Alpha Conversation',
|
||||
)
|
||||
conv_beta = AppConversationInfo(
|
||||
id=uuid4(),
|
||||
created_by_user_id=None,
|
||||
sandbox_id=sandbox_id_beta,
|
||||
title='Beta Conversation',
|
||||
)
|
||||
|
||||
# Mock the info service to return filtered results based on sandbox_id__eq
|
||||
async def mock_search(sandbox_id__eq=None, **kwargs):
|
||||
if sandbox_id__eq == sandbox_id_alpha:
|
||||
return AppConversationInfoPage(items=[conv_alpha])
|
||||
elif sandbox_id__eq == sandbox_id_beta:
|
||||
return AppConversationInfoPage(items=[conv_beta])
|
||||
else:
|
||||
return AppConversationInfoPage(items=[conv_alpha, conv_beta])
|
||||
|
||||
self.mock_app_conversation_info_service.search_app_conversation_info = (
|
||||
AsyncMock(side_effect=mock_search)
|
||||
)
|
||||
|
||||
# Mock sandbox service to return running status for sandbox lookups
|
||||
self.mock_sandbox_service.batch_get_sandboxes = AsyncMock(return_value=[])
|
||||
|
||||
# Test filtering by sandbox_id_alpha
|
||||
result = await self.service.search_app_conversations(
|
||||
sandbox_id__eq=sandbox_id_alpha
|
||||
)
|
||||
|
||||
# Verify the info service was called with the correct sandbox_id__eq
|
||||
self.mock_app_conversation_info_service.search_app_conversation_info.assert_called()
|
||||
call_kwargs = self.mock_app_conversation_info_service.search_app_conversation_info.call_args[
|
||||
1
|
||||
]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id_alpha
|
||||
|
||||
# Verify only alpha conversation is returned
|
||||
assert len(result.items) == 1
|
||||
assert result.items[0].sandbox_id == sandbox_id_alpha
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_count_app_conversations_with_sandbox_id_filter(self):
|
||||
"""Test that count_app_conversations passes sandbox_id__eq to the info service.
|
||||
|
||||
This verifies that the sandbox_id filter is correctly propagated through
|
||||
the service layer to the underlying info service for count operations.
|
||||
"""
|
||||
sandbox_id = 'sandbox-count-test-789'
|
||||
|
||||
# Mock the info service to return count based on sandbox_id__eq
|
||||
async def mock_count(sandbox_id__eq=None, **kwargs):
|
||||
if sandbox_id__eq == sandbox_id:
|
||||
return 3 # 3 conversations match this sandbox
|
||||
else:
|
||||
return 10 # 10 total conversations
|
||||
|
||||
self.mock_app_conversation_info_service.count_app_conversation_info = AsyncMock(
|
||||
side_effect=mock_count
|
||||
)
|
||||
|
||||
# Test counting with sandbox_id filter
|
||||
result = await self.service.count_app_conversations(sandbox_id__eq=sandbox_id)
|
||||
|
||||
# Verify the info service was called with the correct sandbox_id__eq
|
||||
self.mock_app_conversation_info_service.count_app_conversation_info.assert_called_once()
|
||||
call_kwargs = self.mock_app_conversation_info_service.count_app_conversation_info.call_args[
|
||||
1
|
||||
]
|
||||
assert call_kwargs.get('sandbox_id__eq') == sandbox_id
|
||||
|
||||
# Verify filtered count is returned
|
||||
assert result == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_app_conversations_sandbox_id_filter_returns_empty(self):
|
||||
"""Test that search with non-matching sandbox_id returns empty results."""
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversationInfoPage,
|
||||
)
|
||||
|
||||
# Mock the info service to return empty for non-matching sandbox
|
||||
self.mock_app_conversation_info_service.search_app_conversation_info = (
|
||||
AsyncMock(return_value=AppConversationInfoPage(items=[]))
|
||||
)
|
||||
self.mock_sandbox_service.batch_get_sandboxes = AsyncMock(return_value=[])
|
||||
|
||||
# Test filtering by non-existent sandbox_id
|
||||
result = await self.service.search_app_conversations(
|
||||
sandbox_id__eq='non-existent-sandbox'
|
||||
)
|
||||
|
||||
# Verify empty results
|
||||
assert len(result.items) == 0
|
||||
|
||||
|
||||
class TestPluginHandling:
|
||||
"""Test cases for plugin-related functionality in LiveStatusAppConversationService."""
|
||||
|
||||
Reference in New Issue
Block a user