mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
feat(backend): implement API to fetch contents of PLAN.md (#11795)
This commit is contained in:
parent
156d0686c4
commit
d62bb81c3b
@ -1,7 +1,9 @@
|
||||
"""Sandboxed Conversation router for OpenHands Server."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from typing import Annotated, AsyncGenerator
|
||||
from uuid import UUID
|
||||
@ -49,9 +51,21 @@ from openhands.app_server.config import (
|
||||
depends_app_conversation_start_task_service,
|
||||
depends_db_session,
|
||||
depends_httpx_client,
|
||||
depends_sandbox_service,
|
||||
depends_sandbox_spec_service,
|
||||
depends_user_context,
|
||||
get_app_conversation_service,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_models import (
|
||||
AGENT_SERVER,
|
||||
SandboxStatus,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_service import SandboxService
|
||||
from openhands.app_server.sandbox.sandbox_spec_service import SandboxSpecService
|
||||
from openhands.app_server.utils.docker_utils import (
|
||||
replace_localhost_hostname_for_docker,
|
||||
)
|
||||
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
|
||||
|
||||
router = APIRouter(prefix='/app-conversations', tags=['Conversations'])
|
||||
app_conversation_service_dependency = depends_app_conversation_service()
|
||||
@ -61,6 +75,8 @@ app_conversation_start_task_service_dependency = (
|
||||
user_context_dependency = depends_user_context()
|
||||
db_session_dependency = depends_db_session()
|
||||
httpx_client_dependency = depends_httpx_client()
|
||||
sandbox_service_dependency = depends_sandbox_service()
|
||||
sandbox_spec_service_dependency = depends_sandbox_spec_service()
|
||||
|
||||
# Read methods
|
||||
|
||||
@ -289,6 +305,101 @@ async def batch_get_app_conversation_start_tasks(
|
||||
return start_tasks
|
||||
|
||||
|
||||
@router.get('/{conversation_id}/file')
|
||||
async def read_conversation_file(
|
||||
conversation_id: UUID,
|
||||
file_path: Annotated[
|
||||
str,
|
||||
Query(title='Path to the file to read within the sandbox workspace'),
|
||||
] = '/workspace/project/PLAN.md',
|
||||
app_conversation_service: AppConversationService = (
|
||||
app_conversation_service_dependency
|
||||
),
|
||||
sandbox_service: SandboxService = sandbox_service_dependency,
|
||||
sandbox_spec_service: SandboxSpecService = sandbox_spec_service_dependency,
|
||||
) -> str:
|
||||
"""Read a file from a specific conversation's sandbox workspace.
|
||||
|
||||
Returns the content of the file at the specified path if it exists, otherwise returns an empty string.
|
||||
|
||||
Args:
|
||||
conversation_id: The UUID of the conversation
|
||||
file_path: Path to the file to read within the sandbox workspace
|
||||
|
||||
Returns:
|
||||
The content of the file or an empty string if the file doesn't exist
|
||||
"""
|
||||
# Get the conversation info
|
||||
conversation = await app_conversation_service.get_app_conversation(conversation_id)
|
||||
if not conversation:
|
||||
return ''
|
||||
|
||||
# Get the sandbox info
|
||||
sandbox = await sandbox_service.get_sandbox(conversation.sandbox_id)
|
||||
if not sandbox or sandbox.status != SandboxStatus.RUNNING:
|
||||
return ''
|
||||
|
||||
# Get the sandbox spec to find the working directory
|
||||
sandbox_spec = await sandbox_spec_service.get_sandbox_spec(sandbox.sandbox_spec_id)
|
||||
if not sandbox_spec:
|
||||
return ''
|
||||
|
||||
# Get the agent server URL
|
||||
if not sandbox.exposed_urls:
|
||||
return ''
|
||||
|
||||
agent_server_url = None
|
||||
for exposed_url in sandbox.exposed_urls:
|
||||
if exposed_url.name == AGENT_SERVER:
|
||||
agent_server_url = exposed_url.url
|
||||
break
|
||||
|
||||
if not agent_server_url:
|
||||
return ''
|
||||
|
||||
agent_server_url = replace_localhost_hostname_for_docker(agent_server_url)
|
||||
|
||||
# Create remote workspace
|
||||
remote_workspace = AsyncRemoteWorkspace(
|
||||
host=agent_server_url,
|
||||
api_key=sandbox.session_api_key,
|
||||
working_dir=sandbox_spec.working_dir,
|
||||
)
|
||||
|
||||
# Read the file at the specified path
|
||||
temp_file_path = None
|
||||
try:
|
||||
# Create a temporary file path to download the remote file
|
||||
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as temp_file:
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Download the file from remote system
|
||||
result = await remote_workspace.file_download(
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
# Read the content from the temporary file
|
||||
with open(temp_file_path, 'rb') as f:
|
||||
content = f.read()
|
||||
# Decode bytes to string
|
||||
return content.decode('utf-8')
|
||||
except Exception:
|
||||
# If there's any error reading the file, return empty string
|
||||
pass
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
if temp_file_path:
|
||||
try:
|
||||
os.unlink(temp_file_path)
|
||||
except Exception:
|
||||
# Ignore errors during cleanup
|
||||
pass
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
async def _consume_remaining(
|
||||
async_iter, db_session: AsyncSession, httpx_client: httpx.AsyncClient
|
||||
):
|
||||
|
||||
@ -217,7 +217,9 @@ class DockerSandboxService(SandboxService):
|
||||
sandboxes = []
|
||||
|
||||
for container in all_containers:
|
||||
if container.name.startswith(self.container_name_prefix):
|
||||
if container.name and container.name.startswith(
|
||||
self.container_name_prefix
|
||||
):
|
||||
sandbox_info = await self._container_to_checked_sandbox_info(
|
||||
container
|
||||
)
|
||||
|
||||
@ -7,7 +7,7 @@ def stop_all_containers(prefix: str) -> None:
|
||||
containers = docker_client.containers.list(all=True)
|
||||
for container in containers:
|
||||
try:
|
||||
if container.name.startswith(prefix):
|
||||
if container.name and container.name.startswith(prefix):
|
||||
container.stop()
|
||||
except docker.errors.APIError:
|
||||
pass
|
||||
|
||||
@ -15,6 +15,9 @@ from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversation,
|
||||
AppConversationPage,
|
||||
)
|
||||
from openhands.app_server.app_conversation.app_conversation_router import (
|
||||
read_conversation_file,
|
||||
)
|
||||
from openhands.app_server.app_conversation.live_status_app_conversation_service import (
|
||||
LiveStatusAppConversationService,
|
||||
)
|
||||
@ -27,6 +30,7 @@ from openhands.app_server.sandbox.sandbox_models import (
|
||||
SandboxInfo,
|
||||
SandboxStatus,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_spec_models import SandboxSpecInfo
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.integrations.service_types import (
|
||||
AuthenticationError,
|
||||
@ -37,6 +41,10 @@ from openhands.integrations.service_types import (
|
||||
)
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
from openhands.sdk.workspace.models import FileOperationResult
|
||||
from openhands.sdk.workspace.remote.async_remote_workspace import (
|
||||
AsyncRemoteWorkspace,
|
||||
)
|
||||
from openhands.server.data_models.conversation_info import ConversationInfo
|
||||
from openhands.server.data_models.conversation_info_result_set import (
|
||||
ConversationInfoResultSet,
|
||||
@ -980,14 +988,6 @@ async def test_delete_conversation():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_v1_conversation_success():
|
||||
"""Test successful deletion of a V1 conversation."""
|
||||
from uuid import uuid4
|
||||
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversation,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
|
||||
conversation_uuid = uuid4()
|
||||
conversation_id = str(conversation_uuid)
|
||||
|
||||
@ -1060,8 +1060,6 @@ async def test_delete_v1_conversation_success():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_v1_conversation_not_found():
|
||||
"""Test deletion of a V1 conversation that doesn't exist."""
|
||||
from uuid import uuid4
|
||||
|
||||
conversation_uuid = uuid4()
|
||||
conversation_id = str(conversation_uuid)
|
||||
|
||||
@ -1198,8 +1196,6 @@ async def test_delete_v1_conversation_invalid_uuid():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_v1_conversation_service_error():
|
||||
"""Test deletion when app conversation service raises an error."""
|
||||
from uuid import uuid4
|
||||
|
||||
conversation_uuid = uuid4()
|
||||
conversation_id = str(conversation_uuid)
|
||||
|
||||
@ -1293,14 +1289,6 @@ async def test_delete_v1_conversation_service_error():
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_v1_conversation_with_agent_server():
|
||||
"""Test V1 conversation deletion with agent server integration."""
|
||||
from uuid import uuid4
|
||||
|
||||
from openhands.app_server.app_conversation.app_conversation_models import (
|
||||
AppConversation,
|
||||
)
|
||||
from openhands.app_server.sandbox.sandbox_models import SandboxStatus
|
||||
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
||||
|
||||
conversation_uuid = uuid4()
|
||||
conversation_id = str(conversation_uuid)
|
||||
|
||||
@ -2475,3 +2463,919 @@ async def test_delete_v1_conversation_sub_conversation_deletion_error():
|
||||
assert sub2_uuid in delete_calls
|
||||
assert parent_uuid in delete_calls
|
||||
assert sub1_uuid not in delete_calls # Failed before deletion
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_success():
|
||||
"""Test successfully retrieving file content from conversation workspace."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
file_content = '# Project Plan\n\n## Phase 1\n- Task 1\n- Task 2\n'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Mock tempfile and file operations
|
||||
temp_file_path = '/tmp/test_file_12345'
|
||||
mock_file_result = FileOperationResult(
|
||||
success=True,
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
file_size=len(file_content.encode('utf-8')),
|
||||
)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.AsyncRemoteWorkspace'
|
||||
) as mock_workspace_class:
|
||||
mock_workspace = MagicMock(spec=AsyncRemoteWorkspace)
|
||||
mock_workspace.file_download = AsyncMock(return_value=mock_file_result)
|
||||
mock_workspace_class.return_value = mock_workspace
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.tempfile.NamedTemporaryFile'
|
||||
) as mock_tempfile:
|
||||
mock_temp_file = MagicMock()
|
||||
mock_temp_file.name = temp_file_path
|
||||
mock_tempfile.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_temp_file
|
||||
)
|
||||
mock_tempfile.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_file_handle = MagicMock()
|
||||
mock_file_handle.read.return_value = file_content.encode('utf-8')
|
||||
mock_open.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_file_handle
|
||||
)
|
||||
mock_open.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.os.unlink'
|
||||
) as mock_unlink:
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == file_content
|
||||
|
||||
# Verify services were called correctly
|
||||
mock_app_conversation_service.get_app_conversation.assert_called_once_with(
|
||||
conversation_id
|
||||
)
|
||||
mock_sandbox_service.get_sandbox.assert_called_once_with(
|
||||
'test-sandbox-id'
|
||||
)
|
||||
mock_sandbox_spec_service.get_sandbox_spec.assert_called_once_with(
|
||||
'test-spec-id'
|
||||
)
|
||||
|
||||
# Verify workspace was created and file_download was called
|
||||
mock_workspace_class.assert_called_once()
|
||||
mock_workspace.file_download.assert_called_once_with(
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
)
|
||||
|
||||
# Verify file was read and cleaned up
|
||||
mock_open.assert_called_once_with(temp_file_path, 'rb')
|
||||
mock_unlink.assert_called_once_with(temp_file_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_different_path():
|
||||
"""Test successfully retrieving file content from a different file path."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/src/main.py'
|
||||
file_content = 'def main():\n print("Hello, World!")\n'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Mock tempfile and file operations
|
||||
temp_file_path = '/tmp/test_file_67890'
|
||||
mock_file_result = FileOperationResult(
|
||||
success=True,
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
file_size=len(file_content.encode('utf-8')),
|
||||
)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.AsyncRemoteWorkspace'
|
||||
) as mock_workspace_class:
|
||||
mock_workspace = MagicMock(spec=AsyncRemoteWorkspace)
|
||||
mock_workspace.file_download = AsyncMock(return_value=mock_file_result)
|
||||
mock_workspace_class.return_value = mock_workspace
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.tempfile.NamedTemporaryFile'
|
||||
) as mock_tempfile:
|
||||
mock_temp_file = MagicMock()
|
||||
mock_temp_file.name = temp_file_path
|
||||
mock_tempfile.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_temp_file
|
||||
)
|
||||
mock_tempfile.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_file_handle = MagicMock()
|
||||
mock_file_handle.read.return_value = file_content.encode('utf-8')
|
||||
mock_open.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_file_handle
|
||||
)
|
||||
mock_open.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.os.unlink'
|
||||
) as mock_unlink:
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == file_content
|
||||
|
||||
# Verify workspace was created and file_download was called
|
||||
mock_workspace_class.assert_called_once()
|
||||
mock_workspace.file_download.assert_called_once_with(
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
)
|
||||
|
||||
# Verify file was read and cleaned up
|
||||
mock_open.assert_called_once_with(temp_file_path, 'rb')
|
||||
mock_unlink.assert_called_once_with(temp_file_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_conversation_not_found():
|
||||
"""Test when conversation doesn't exist."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(return_value=None)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
# Verify only conversation service was called
|
||||
mock_app_conversation_service.get_app_conversation.assert_called_once_with(
|
||||
conversation_id
|
||||
)
|
||||
mock_sandbox_service.get_sandbox.assert_not_called()
|
||||
mock_sandbox_spec_service.get_sandbox_spec.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_sandbox_not_found():
|
||||
"""Test when sandbox doesn't exist."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=None)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
# Verify services were called
|
||||
mock_app_conversation_service.get_app_conversation.assert_called_once_with(
|
||||
conversation_id
|
||||
)
|
||||
mock_sandbox_service.get_sandbox.assert_called_once_with('test-sandbox-id')
|
||||
mock_sandbox_spec_service.get_sandbox_spec.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_sandbox_not_running():
|
||||
"""Test when sandbox is not in RUNNING status."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.PAUSED,
|
||||
execution_status=None,
|
||||
session_api_key=None,
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.PAUSED,
|
||||
session_api_key=None,
|
||||
exposed_urls=None,
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
# Verify services were called
|
||||
mock_app_conversation_service.get_app_conversation.assert_called_once_with(
|
||||
conversation_id
|
||||
)
|
||||
mock_sandbox_service.get_sandbox.assert_called_once_with('test-sandbox-id')
|
||||
mock_sandbox_spec_service.get_sandbox_spec.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_sandbox_spec_not_found():
|
||||
"""Test when sandbox spec doesn't exist."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(return_value=None)
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
# Verify services were called
|
||||
mock_app_conversation_service.get_app_conversation.assert_called_once_with(
|
||||
conversation_id
|
||||
)
|
||||
mock_sandbox_service.get_sandbox.assert_called_once_with('test-sandbox-id')
|
||||
mock_sandbox_spec_service.get_sandbox_spec.assert_called_once_with('test-spec-id')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_no_exposed_urls():
|
||||
"""Test when sandbox has no exposed URLs."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox with no exposed URLs
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=None,
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_no_agent_server_url():
|
||||
"""Test when sandbox has exposed URLs but no AGENT_SERVER."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox with exposed URLs but no AGENT_SERVER
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name='OTHER_SERVICE', url='http://other:9000', port=9000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result
|
||||
assert result == ''
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_file_not_found():
|
||||
"""Test when file doesn't exist."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Mock tempfile and file operations for file not found
|
||||
temp_file_path = '/tmp/test_file_not_found'
|
||||
mock_file_result = FileOperationResult(
|
||||
success=False,
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
error=f'File not found: {file_path}',
|
||||
)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.AsyncRemoteWorkspace'
|
||||
) as mock_workspace_class:
|
||||
mock_workspace = MagicMock(spec=AsyncRemoteWorkspace)
|
||||
mock_workspace.file_download = AsyncMock(return_value=mock_file_result)
|
||||
mock_workspace_class.return_value = mock_workspace
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.tempfile.NamedTemporaryFile'
|
||||
) as mock_tempfile:
|
||||
mock_temp_file = MagicMock()
|
||||
mock_temp_file.name = temp_file_path
|
||||
mock_tempfile.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_temp_file
|
||||
)
|
||||
mock_tempfile.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.os.unlink'
|
||||
) as mock_unlink:
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result (empty string when file_download fails)
|
||||
assert result == ''
|
||||
|
||||
# Verify cleanup still happens
|
||||
mock_unlink.assert_called_once_with(temp_file_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_empty_file():
|
||||
"""Test when file exists but is empty."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Mock tempfile and file operations for empty file
|
||||
temp_file_path = '/tmp/test_file_empty'
|
||||
empty_content = ''
|
||||
mock_file_result = FileOperationResult(
|
||||
success=True,
|
||||
source_path=file_path,
|
||||
destination_path=temp_file_path,
|
||||
file_size=0,
|
||||
)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.AsyncRemoteWorkspace'
|
||||
) as mock_workspace_class:
|
||||
mock_workspace = MagicMock(spec=AsyncRemoteWorkspace)
|
||||
mock_workspace.file_download = AsyncMock(return_value=mock_file_result)
|
||||
mock_workspace_class.return_value = mock_workspace
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.tempfile.NamedTemporaryFile'
|
||||
) as mock_tempfile:
|
||||
mock_temp_file = MagicMock()
|
||||
mock_temp_file.name = temp_file_path
|
||||
mock_tempfile.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_temp_file
|
||||
)
|
||||
mock_tempfile.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch('builtins.open', create=True) as mock_open:
|
||||
mock_file_handle = MagicMock()
|
||||
mock_file_handle.read.return_value = empty_content.encode('utf-8')
|
||||
mock_open.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_file_handle
|
||||
)
|
||||
mock_open.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.os.unlink'
|
||||
) as mock_unlink:
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result (empty string when file is empty)
|
||||
assert result == ''
|
||||
|
||||
# Verify cleanup happens
|
||||
mock_unlink.assert_called_once_with(temp_file_path)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_conversation_file_command_exception():
|
||||
"""Test when command execution raises an exception."""
|
||||
conversation_id = uuid4()
|
||||
file_path = '/workspace/project/PLAN.md'
|
||||
|
||||
# Mock conversation
|
||||
mock_conversation = AppConversation(
|
||||
id=conversation_id,
|
||||
created_by_user_id='test_user',
|
||||
sandbox_id='test-sandbox-id',
|
||||
title='Test Conversation',
|
||||
sandbox_status=SandboxStatus.RUNNING,
|
||||
execution_status=ConversationExecutionStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
selected_repository='test/repo',
|
||||
selected_branch='main',
|
||||
git_provider=ProviderType.GITHUB,
|
||||
trigger=ConversationTrigger.GUI,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock sandbox
|
||||
mock_sandbox = SandboxInfo(
|
||||
id='test-sandbox-id',
|
||||
created_by_user_id='test_user',
|
||||
sandbox_spec_id='test-spec-id',
|
||||
status=SandboxStatus.RUNNING,
|
||||
session_api_key='test-api-key',
|
||||
exposed_urls=[
|
||||
ExposedUrl(name=AGENT_SERVER, url='http://agent:8000', port=8000)
|
||||
],
|
||||
)
|
||||
|
||||
# Mock sandbox spec
|
||||
mock_sandbox_spec = SandboxSpecInfo(
|
||||
id='test-spec-id',
|
||||
command=None,
|
||||
working_dir='/workspace',
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
# Mock services
|
||||
mock_app_conversation_service = MagicMock()
|
||||
mock_app_conversation_service.get_app_conversation = AsyncMock(
|
||||
return_value=mock_conversation
|
||||
)
|
||||
|
||||
mock_sandbox_service = MagicMock()
|
||||
mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
|
||||
|
||||
mock_sandbox_spec_service = MagicMock()
|
||||
mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
|
||||
return_value=mock_sandbox_spec
|
||||
)
|
||||
|
||||
# Mock tempfile and file operations for exception case
|
||||
temp_file_path = '/tmp/test_file_exception'
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.AsyncRemoteWorkspace'
|
||||
) as mock_workspace_class:
|
||||
mock_workspace = MagicMock(spec=AsyncRemoteWorkspace)
|
||||
mock_workspace.file_download = AsyncMock(
|
||||
side_effect=Exception('Connection timeout')
|
||||
)
|
||||
mock_workspace_class.return_value = mock_workspace
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.tempfile.NamedTemporaryFile'
|
||||
) as mock_tempfile:
|
||||
mock_temp_file = MagicMock()
|
||||
mock_temp_file.name = temp_file_path
|
||||
mock_tempfile.return_value.__enter__ = MagicMock(
|
||||
return_value=mock_temp_file
|
||||
)
|
||||
mock_tempfile.return_value.__exit__ = MagicMock(return_value=None)
|
||||
|
||||
with patch(
|
||||
'openhands.app_server.app_conversation.app_conversation_router.os.unlink'
|
||||
) as mock_unlink:
|
||||
# Call the endpoint
|
||||
result = await read_conversation_file(
|
||||
conversation_id=conversation_id,
|
||||
file_path=file_path,
|
||||
app_conversation_service=mock_app_conversation_service,
|
||||
sandbox_service=mock_sandbox_service,
|
||||
sandbox_spec_service=mock_sandbox_spec_service,
|
||||
)
|
||||
|
||||
# Verify result (empty string on exception)
|
||||
assert result == ''
|
||||
|
||||
# Verify cleanup still happens even on exception
|
||||
mock_unlink.assert_called_once_with(temp_file_path)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user