diff --git a/openhands/app_server/app_conversation/live_status_app_conversation_service.py b/openhands/app_server/app_conversation/live_status_app_conversation_service.py index e7641ad487..a8d490489c 100644 --- a/openhands/app_server/app_conversation/live_status_app_conversation_service.py +++ b/openhands/app_server/app_conversation/live_status_app_conversation_service.py @@ -270,7 +270,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): user_id = await self.user_context.get_user_id() app_conversation_info = AppConversationInfo( id=info.id, - title=f'Conversation {info.id.hex}', + title=f'Conversation {info.id.hex[:5]}', sandbox_id=sandbox.id, created_by_user_id=user_id, llm_model=start_conversation_request.agent.llm.model, diff --git a/tests/unit/app_server/test_live_status_app_conversation_service.py b/tests/unit/app_server/test_live_status_app_conversation_service.py index 6a9821b9f3..f662f33146 100644 --- a/tests/unit/app_server/test_live_status_app_conversation_service.py +++ b/tests/unit/app_server/test_live_status_app_conversation_service.py @@ -6,11 +6,20 @@ from uuid import UUID, uuid4 import pytest from openhands.agent_server.models import SendMessageRequest, StartConversationRequest -from openhands.app_server.app_conversation.app_conversation_models import AgentType +from openhands.app_server.app_conversation.app_conversation_models import ( + AgentType, + AppConversationStartRequest, +) from openhands.app_server.app_conversation.live_status_app_conversation_service import ( LiveStatusAppConversationService, ) -from openhands.app_server.sandbox.sandbox_models import SandboxInfo, SandboxStatus +from openhands.app_server.sandbox.sandbox_models import ( + AGENT_SERVER, + ExposedUrl, + SandboxInfo, + SandboxStatus, +) +from openhands.app_server.sandbox.sandbox_spec_models import SandboxSpecInfo from openhands.app_server.user.user_context import UserContext from openhands.integrations.provider import ProviderType from openhands.sdk import Agent @@ -842,6 +851,109 @@ class TestLiveStatusAppConversationService: ) self.service._finalize_conversation_request.assert_called_once() + @pytest.mark.asyncio + @patch( + 'openhands.app_server.app_conversation.live_status_app_conversation_service.AsyncRemoteWorkspace' + ) + @patch( + 'openhands.app_server.app_conversation.live_status_app_conversation_service.ConversationInfo' + ) + async def test_start_app_conversation_default_title_uses_first_five_characters( + self, mock_conversation_info_class, mock_remote_workspace_class + ): + """Test that v1 conversations use first 5 characters of conversation ID for default title.""" + # Arrange + conversation_id = uuid4() + conversation_id_hex = conversation_id.hex + expected_title = f'Conversation {conversation_id_hex[:5]}' + + # Mock user context + self.mock_user_context.get_user_id = AsyncMock(return_value='test_user_123') + self.mock_user_context.get_user_info = AsyncMock(return_value=self.mock_user) + + # Mock sandbox and sandbox spec + mock_sandbox_spec = Mock(spec=SandboxSpecInfo) + mock_sandbox_spec.working_dir = '/test/workspace' + self.mock_sandbox.sandbox_spec_id = str(uuid4()) + self.mock_sandbox.id = str(uuid4()) # Ensure sandbox.id is a string + self.mock_sandbox.session_api_key = 'test_session_key' + exposed_url = ExposedUrl( + name=AGENT_SERVER, url='http://agent-server:8000', port=60000 + ) + self.mock_sandbox.exposed_urls = [exposed_url] + + self.mock_sandbox_service.get_sandbox = AsyncMock( + return_value=self.mock_sandbox + ) + self.mock_sandbox_spec_service.get_sandbox_spec = AsyncMock( + return_value=mock_sandbox_spec + ) + + # Mock remote workspace + mock_remote_workspace = Mock() + mock_remote_workspace_class.return_value = mock_remote_workspace + + # Mock the wait for sandbox and setup scripts + async def mock_wait_for_sandbox(task): + task.sandbox_id = self.mock_sandbox.id + yield task + + async def mock_run_setup_scripts(task, sandbox, workspace): + yield task + + self.service._wait_for_sandbox_start = mock_wait_for_sandbox + self.service.run_setup_scripts = mock_run_setup_scripts + + # Mock build start conversation request + mock_agent = Mock(spec=Agent) + mock_agent.llm = Mock(spec=LLM) + mock_agent.llm.model = 'gpt-4' + mock_start_request = Mock(spec=StartConversationRequest) + mock_start_request.agent = mock_agent + mock_start_request.model_dump.return_value = {'test': 'data'} + + self.service._build_start_conversation_request_for_user = AsyncMock( + return_value=mock_start_request + ) + + # Mock ConversationInfo returned from agent server + mock_conversation_info = Mock() + mock_conversation_info.id = conversation_id + mock_conversation_info_class.model_validate.return_value = ( + mock_conversation_info + ) + + # Mock HTTP response from agent server + mock_response = Mock() + mock_response.json.return_value = {'id': str(conversation_id)} + mock_response.raise_for_status = Mock() + self.mock_httpx_client.post = AsyncMock(return_value=mock_response) + + # Mock event callback service + self.mock_event_callback_service.save_event_callback = AsyncMock() + + # Create request + request = AppConversationStartRequest() + + # Act + async for task in self.service._start_app_conversation(request): + # Consume all tasks to reach the point where title is set + pass + + # Assert + # Verify that save_app_conversation_info was called with the correct title format + self.mock_app_conversation_info_service.save_app_conversation_info.assert_called_once() + call_args = ( + self.mock_app_conversation_info_service.save_app_conversation_info.call_args + ) + saved_info = call_args[0][0] # First positional argument + + assert saved_info.title == expected_title, ( + f'Expected title to be "{expected_title}" (first 5 chars), ' + f'but got "{saved_info.title}"' + ) + assert saved_info.id == conversation_id + @pytest.mark.asyncio async def test_configure_llm_and_mcp_with_custom_sse_servers(self): """Test _configure_llm_and_mcp merges custom SSE servers with UUID-based names."""