Compare commits

...

2 Commits

Author SHA1 Message Date
openhands 77ab9c7387 Fix test_download_file to browse to correct HTML page
- Change from browsing root directory to specific download_test.html page
- Ensures test assertion matches actual browser behavior
2025-10-30 08:12:45 +00:00
Rohit Malhotra 2fe513410a CLI: bump agent-sdk version (#11566)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-30 07:29:50 +00:00
13 changed files with 157 additions and 79 deletions
+3 -4
View File
@@ -15,13 +15,12 @@ import sys
import time
from pathlib import Path
from openhands_cli.llm_utils import get_llm_metadata
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR, WORK_DIR
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
from openhands.sdk import LLM
from openhands.tools.preset.default import get_default_agent
dummy_agent = get_default_agent(
dummy_agent = get_default_cli_agent(
llm=LLM(
model='dummy-model',
api_key='dummy-key',
+1
View File
@@ -120,6 +120,7 @@ class ConversationRunner:
else:
raise Exception('Infinite loop')
def _handle_confirmation_request(self) -> UserConfirmation:
"""Handle confirmation request from user.
@@ -1,13 +1,11 @@
import os
from openhands.sdk import LLM, BaseConversation, LocalFileStore
from openhands.sdk.security.confirmation_policy import NeverConfirm
from openhands.tools.preset.default import get_default_agent
from prompt_toolkit import HTML, print_formatted_text
from prompt_toolkit.shortcuts import print_container
from prompt_toolkit.widgets import Frame, TextArea
from openhands_cli.llm_utils import get_llm_metadata
from openhands_cli.utils import get_llm_metadata, get_default_cli_agent
from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
from openhands_cli.pt_style import COLOR_GREY
from openhands_cli.tui.settings.store import AgentStore
@@ -182,7 +180,7 @@ class SettingsScreen:
agent = self.agent_store.load()
if not agent:
agent = get_default_agent(llm=llm, cli_mode=True)
agent = get_default_cli_agent(llm=llm)
agent = agent.model_copy(update={'llm': llm})
self.agent_store.save(agent)
@@ -5,7 +5,7 @@ from pathlib import Path
from typing import Any
from fastmcp.mcp_config import MCPConfig
from openhands_cli.llm_utils import get_llm_metadata
from openhands_cli.utils import get_llm_metadata
from openhands_cli.locations import (
AGENT_SETTINGS_PATH,
MCP_CONFIG_FILE,
@@ -2,7 +2,9 @@
import os
from typing import Any
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
from openhands.tools.preset import get_default_agent
from openhands.sdk import LLM
def get_llm_metadata(
model_name: str,
@@ -55,3 +57,20 @@ def get_llm_metadata(
if user_id is not None:
metadata['trace_user_id'] = user_id
return metadata
def get_default_cli_agent(
llm: LLM
):
agent = get_default_agent(
llm=llm,
cli_mode=True
)
agent = agent.model_copy(
update={
'security_analyzer': LLMSecurityAnalyzer()
}
)
return agent
+2 -2
View File
@@ -18,8 +18,8 @@ classifiers = [
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
# TODO: pin package versions once agent-sdk has published PyPI packages
dependencies = [
"openhands-sdk==1.0.0a3",
"openhands-tools==1.0.0a3",
"openhands-sdk==1.0.0a5",
"openhands-tools==1.0.0a5",
"prompt-toolkit>=3",
"typer>=0.17.4",
]
@@ -6,10 +6,10 @@ import pytest
from openhands_cli.tui.settings.settings_screen import SettingsScreen
from openhands_cli.tui.settings.store import AgentStore
from openhands_cli.user_actions.settings_action import SettingsType
from openhands_cli.utils import get_default_cli_agent
from pydantic import SecretStr
from openhands.sdk import LLM, Conversation, LocalFileStore
from openhands.tools.preset.default import get_default_agent
def read_json(path: Path) -> dict:
@@ -30,7 +30,7 @@ def make_screen_with_conversation(model='openai/gpt-4o-mini', api_key='sk-xyz'):
def seed_file(path: Path, model: str = 'openai/gpt-4o-mini', api_key: str = 'sk-old'):
store = AgentStore()
store.file_store = LocalFileStore(root=str(path))
agent = get_default_agent(
agent = get_default_cli_agent(
llm=LLM(model=model, api_key=SecretStr(api_key), service_id='test-service')
)
store.save(agent)
@@ -6,13 +6,13 @@ from openhands_cli.runner import ConversationRunner
from openhands_cli.user_actions.types import UserConfirmation
from pydantic import ConfigDict, SecretStr, model_validator
from openhands.sdk import Conversation, ConversationCallbackType
from openhands.sdk import Conversation, ConversationCallbackType, LocalConversation
from openhands.sdk.agent.base import AgentBase
from openhands.sdk.conversation import ConversationState
from openhands.sdk.conversation.state import AgentExecutionStatus
from openhands.sdk.llm import LLM
from openhands.sdk.security.confirmation_policy import AlwaysConfirm, NeverConfirm
from unittest.mock import MagicMock
class FakeLLM(LLM):
@model_validator(mode='after')
@@ -41,11 +41,11 @@ class FakeAgent(AgentBase):
pass
def step(
self, state: ConversationState, on_event: ConversationCallbackType
self, conversation: LocalConversation, on_event: ConversationCallbackType
) -> None:
self.step_count += 1
if self.step_count == self.finish_on_step:
state.agent_status = AgentExecutionStatus.FINISHED
conversation.state.agent_status = AgentExecutionStatus.FINISHED
@pytest.fixture()
@@ -102,15 +102,15 @@ class TestConversationRunner:
"""
if final_status == AgentExecutionStatus.FINISHED:
agent.finish_on_step = 1
# Add a mock security analyzer to enable confirmation mode
from unittest.mock import MagicMock
agent.security_analyzer = MagicMock()
convo = Conversation(agent)
convo.state.agent_status = AgentExecutionStatus.WAITING_FOR_CONFIRMATION
cr = ConversationRunner(convo)
cr.set_confirmation_policy(AlwaysConfirm())
with patch.object(
cr, '_handle_confirmation_request', return_value=confirmation
) as mock_confirmation_request:
+9 -9
View File
@@ -1828,7 +1828,7 @@ wheels = [
[[package]]
name = "openhands"
version = "1.0.1"
version = "1.0.2"
source = { editable = "." }
dependencies = [
{ name = "openhands-sdk" },
@@ -1855,8 +1855,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "openhands-sdk", specifier = "==1.0.0a3" },
{ name = "openhands-tools", specifier = "==1.0.0a3" },
{ name = "openhands-sdk", specifier = "==1.0.0a5" },
{ name = "openhands-tools", specifier = "==1.0.0a5" },
{ name = "prompt-toolkit", specifier = ">=3" },
{ name = "typer", specifier = ">=0.17.4" },
]
@@ -1879,7 +1879,7 @@ dev = [
[[package]]
name = "openhands-sdk"
version = "1.0.0a3"
version = "1.0.0a5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "fastmcp" },
@@ -1891,14 +1891,14 @@ dependencies = [
{ name = "tenacity" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/82/33b3e3560e259803b773eee9cb377fce63b56c4252f3036126e225171926/openhands_sdk-1.0.0a3.tar.gz", hash = "sha256:c2cf6ab2ac105d257a31fde0e502a81faa969c7e64e0b2364d0634d2ce8e93b4", size = 144940, upload-time = "2025-10-20T15:38:39.647Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1a/90/d40f6716641a95a61d2042f00855e0eadc0b2558167078324576cc5a3c22/openhands_sdk-1.0.0a5.tar.gz", hash = "sha256:8888d6892d58cf9b11a71fa80086156c0b6c9a0b50df6839c0a9cafffba2338c", size = 152810, upload-time = "2025-10-29T16:19:52.086Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/ab/4464d2470ef1e02334f9ade094dfefa2cfc5bb761b201663a3e4121e1892/openhands_sdk-1.0.0a3-py3-none-any.whl", hash = "sha256:c8ab45160b67e7de391211ae5607ccfdf44e39781f74d115a2a22df35a2f4311", size = 191937, upload-time = "2025-10-20T15:38:38.668Z" },
{ url = "https://files.pythonhosted.org/packages/00/6b/d3aa28019163f22f4b589ad818b83e3bea23d0a50b0c51ecc070ffdec139/openhands_sdk-1.0.0a5-py3-none-any.whl", hash = "sha256:db20272b04cf03627f9f7d1e87992078ac4ce15d188955a2962aa9e754d0af03", size = 204063, upload-time = "2025-10-29T16:19:50.684Z" },
]
[[package]]
name = "openhands-tools"
version = "1.0.0a3"
version = "1.0.0a5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bashlex" },
@@ -1910,9 +1910,9 @@ dependencies = [
{ name = "openhands-sdk" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d6/93/53cf1a5ae97e0c23d7e024db5bbb1ba1da9855c6352cc91d6b65fc6f5e13/openhands_tools-1.0.0a3.tar.gz", hash = "sha256:2a15fff3749ee5856906ffce999fec49c8305e7f9911f05e01dbcf4ea772e385", size = 59103, upload-time = "2025-10-20T15:38:43.705Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/8d/d62bc5e6c986676363692743688f10b6a922fd24dd525e5c6e87bd6fc08e/openhands_tools-1.0.0a5.tar.gz", hash = "sha256:6c67454e612596e95c5151267659ddd3b633a5d4a1b70b348f7f913c62146562", size = 63012, upload-time = "2025-10-29T16:19:53.783Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/aa/251ce4ecd560cad295e1c81def9efadfd1009cec3b7e79bd41357c6a0670/openhands_tools-1.0.0a3-py3-none-any.whl", hash = "sha256:f4c81df682c2a1a1c0bfa450bfe25ba9de5a6a3b56d6bab90f7541bf149bb3ed", size = 78814, upload-time = "2025-10-20T15:38:42.795Z" },
{ url = "https://files.pythonhosted.org/packages/07/9d/4da48258f0af73d017b61ed3f12786fae4caccc7e7cd97d77ef2bb25f00c/openhands_tools-1.0.0a5-py3-none-any.whl", hash = "sha256:74c27e23e6adc9a0bad00e32448bd4872019ce0786474e8de2fbf2d7c0887e8e", size = 84724, upload-time = "2025-10-29T16:19:52.84Z" },
]
[[package]]
+41
View File
@@ -17,6 +17,7 @@ from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
from openhands.runtime.impl.local.local_runtime import LocalRuntime
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
from openhands.runtime.plugins import AgentSkillsRequirement, JupyterRequirement
from openhands.runtime.utils.port_lock import find_available_port_with_lock
from openhands.storage import get_file_store
from openhands.utils.async_utils import call_async_from_sync
@@ -294,9 +295,49 @@ def _load_runtime(
return runtime, runtime.config
# Port range for test HTTP servers (separate from runtime ports to avoid conflicts)
TEST_HTTP_SERVER_PORT_RANGE = (18000, 18999)
@pytest.fixture
def dynamic_port(request):
"""Allocate a dynamic port with locking to prevent race conditions in parallel tests.
This fixture uses the existing port locking system to ensure that parallel test
workers don't try to use the same port for HTTP servers.
Returns:
int: An available port number that is locked for this test
"""
result = find_available_port_with_lock(
min_port=TEST_HTTP_SERVER_PORT_RANGE[0],
max_port=TEST_HTTP_SERVER_PORT_RANGE[1],
max_attempts=20,
bind_address='0.0.0.0',
lock_timeout=2.0,
)
if result is None:
pytest.fail(
f'Could not allocate a dynamic port in range {TEST_HTTP_SERVER_PORT_RANGE}'
)
port, port_lock = result
logger.info(f'Allocated dynamic port {port} for test {request.node.name}')
def cleanup():
if port_lock:
port_lock.release()
logger.info(f'Released dynamic port {port} for test {request.node.name}')
request.addfinalizer(cleanup)
return port
# Export necessary function
__all__ = [
'_load_runtime',
'_get_host_folder',
'_remove_folder',
'dynamic_port',
]
+5 -5
View File
@@ -51,11 +51,11 @@ def get_platform_command(linux_cmd, windows_cmd):
return windows_cmd if is_windows() else linux_cmd
def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
def test_bash_server(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
try:
# Use python -u for unbuffered output, potentially helping capture initial output on Windows
action = CmdRunAction(command='python -u -m http.server 8081')
action = CmdRunAction(command=f'python -u -m http.server {dynamic_port}')
action.set_hard_timeout(1)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -110,7 +110,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
assert config.workspace_mount_path_in_sandbox in obs.metadata.working_dir
# run it again!
action = CmdRunAction(command='python -u -m http.server 8081')
action = CmdRunAction(command=f'python -u -m http.server {dynamic_port}')
action.set_hard_timeout(1)
obs = runtime.run_action(action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -122,9 +122,9 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_bash_background_server(temp_dir, runtime_cls, run_as_openhands):
def test_bash_background_server(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands)
server_port = 8081
server_port = dynamic_port
try:
# Start the server, expect it to timeout (run in background manner)
action = CmdRunAction(f'python3 -m http.server {server_port} &')
+46 -32
View File
@@ -123,17 +123,21 @@ def find_element_by_tag_and_attributes(
return None
def test_browser_disabled(temp_dir, runtime_cls, run_as_openhands):
def test_browser_disabled(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
runtime, _ = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=False
)
action_cmd = CmdRunAction(command='python3 -m http.server 8000 > server.log 2>&1 &')
action_cmd = CmdRunAction(
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
action_browse = BrowseURLAction(url='http://localhost:8000', return_axtree=False)
action_browse = BrowseURLAction(
url=f'http://localhost:{dynamic_port}', return_axtree=False
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_browse)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -143,13 +147,15 @@ def test_browser_disabled(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_simple_browse(temp_dir, runtime_cls, run_as_openhands):
def test_simple_browse(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
)
# Test browse
action_cmd = CmdRunAction(command='python3 -m http.server 8000 > server.log 2>&1 &')
action_cmd = CmdRunAction(
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -164,17 +170,19 @@ def test_simple_browse(temp_dir, runtime_cls, run_as_openhands):
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
action_browse = BrowseURLAction(url='http://localhost:8000', return_axtree=False)
action_browse = BrowseURLAction(
url=f'http://localhost:{dynamic_port}', return_axtree=False
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_browse)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert isinstance(obs, BrowserOutputObservation)
assert 'http://localhost:8000' in obs.url
assert f'http://localhost:{dynamic_port}' in obs.url
assert not obs.error
assert obs.open_pages_urls == ['http://localhost:8000/']
assert obs.open_pages_urls == [f'http://localhost:{dynamic_port}/']
assert obs.active_page_index == 0
assert obs.last_browser_action == 'goto("http://localhost:8000")'
assert obs.last_browser_action == f'goto("http://localhost:{dynamic_port}")'
assert obs.last_browser_action_error == ''
assert 'Directory listing for /' in obs.content
assert 'server.log' in obs.content
@@ -189,7 +197,9 @@ def test_simple_browse(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
def test_browser_navigation_actions(
temp_dir, runtime_cls, run_as_openhands, dynamic_port
):
"""Test browser navigation actions: goto, go_back, go_forward, noop."""
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
@@ -234,7 +244,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
# Start HTTP server
action_cmd = CmdRunAction(
command='python3 -m http.server 8000 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
@@ -249,7 +259,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
# Test goto action
action_browse = BrowseInteractiveAction(
browser_actions='goto("http://localhost:8000/page1.html")',
browser_actions=f'goto("http://localhost:{dynamic_port}/page1.html")',
return_axtree=False,
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
@@ -259,7 +269,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
assert isinstance(obs, BrowserOutputObservation)
assert not obs.error
assert 'Page 1' in obs.content
assert 'http://localhost:8000/page1.html' in obs.url
assert f'http://localhost:{dynamic_port}/page1.html' in obs.url
# Test noop action (should not change page)
action_browse = BrowseInteractiveAction(
@@ -272,11 +282,11 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
assert isinstance(obs, BrowserOutputObservation)
assert not obs.error
assert 'Page 1' in obs.content
assert 'http://localhost:8000/page1.html' in obs.url
assert f'http://localhost:{dynamic_port}/page1.html' in obs.url
# Navigate to page 2
action_browse = BrowseInteractiveAction(
browser_actions='goto("http://localhost:8000/page2.html")',
browser_actions=f'goto("http://localhost:{dynamic_port}/page2.html")',
return_axtree=False,
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
@@ -286,7 +296,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
assert isinstance(obs, BrowserOutputObservation)
assert not obs.error
assert 'Page 2' in obs.content
assert 'http://localhost:8000/page2.html' in obs.url
assert f'http://localhost:{dynamic_port}/page2.html' in obs.url
# Test go_back action
action_browse = BrowseInteractiveAction(
@@ -299,7 +309,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
assert isinstance(obs, BrowserOutputObservation)
assert not obs.error
assert 'Page 1' in obs.content
assert 'http://localhost:8000/page1.html' in obs.url
assert f'http://localhost:{dynamic_port}/page1.html' in obs.url
# Test go_forward action
action_browse = BrowseInteractiveAction(
@@ -312,7 +322,7 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
assert isinstance(obs, BrowserOutputObservation)
assert not obs.error
assert 'Page 2' in obs.content
assert 'http://localhost:8000/page2.html' in obs.url
assert f'http://localhost:{dynamic_port}/page2.html' in obs.url
# Clean up
action_cmd = CmdRunAction(command='pkill -f "python3 -m http.server" || true')
@@ -324,7 +334,9 @@ def test_browser_navigation_actions(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_browser_form_interactions(temp_dir, runtime_cls, run_as_openhands):
def test_browser_form_interactions(
temp_dir, runtime_cls, run_as_openhands, dynamic_port
):
"""Test browser form interaction actions: fill, click, select_option, clear."""
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
@@ -370,7 +382,7 @@ def test_browser_form_interactions(temp_dir, runtime_cls, run_as_openhands):
# Start HTTP server
action_cmd = CmdRunAction(
command='python3 -m http.server 8000 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
@@ -385,7 +397,7 @@ def test_browser_form_interactions(temp_dir, runtime_cls, run_as_openhands):
# Navigate to form page
action_browse = BrowseInteractiveAction(
browser_actions='goto("http://localhost:8000/form.html")',
browser_actions=f'goto("http://localhost:{dynamic_port}/form.html")',
return_axtree=True, # Need axtree to get element bids
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
@@ -540,7 +552,9 @@ fill("{textarea_bid}", "This is a test message")
_close_test_runtime(runtime)
def test_browser_interactive_actions(temp_dir, runtime_cls, run_as_openhands):
def test_browser_interactive_actions(
temp_dir, runtime_cls, run_as_openhands, dynamic_port
):
"""Test browser interactive actions: scroll, hover, fill, press, focus."""
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
@@ -587,7 +601,7 @@ def test_browser_interactive_actions(temp_dir, runtime_cls, run_as_openhands):
# Start HTTP server
action_cmd = CmdRunAction(
command='python3 -m http.server 8000 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
@@ -602,7 +616,7 @@ def test_browser_interactive_actions(temp_dir, runtime_cls, run_as_openhands):
# Navigate to scroll page
action_browse = BrowseInteractiveAction(
browser_actions='goto("http://localhost:8000/scroll.html")',
browser_actions=f'goto("http://localhost:{dynamic_port}/scroll.html")',
return_axtree=True,
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
@@ -748,7 +762,7 @@ scroll(0, 400)
_close_test_runtime(runtime)
def test_browser_file_upload(temp_dir, runtime_cls, run_as_openhands):
def test_browser_file_upload(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
"""Test browser file upload action."""
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
@@ -799,7 +813,7 @@ def test_browser_file_upload(temp_dir, runtime_cls, run_as_openhands):
# Start HTTP server
action_cmd = CmdRunAction(
command='python3 -m http.server 8000 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
@@ -814,7 +828,7 @@ def test_browser_file_upload(temp_dir, runtime_cls, run_as_openhands):
# Navigate to upload page
action_browse = BrowseInteractiveAction(
browser_actions='goto("http://localhost:8000/upload.html")',
browser_actions=f'goto("http://localhost:{dynamic_port}/upload.html")',
return_axtree=True,
)
logger.info(action_browse, extra={'msg_type': 'ACTION'})
@@ -1049,7 +1063,7 @@ def test_read_png_browse(temp_dir, runtime_cls, run_as_openhands):
_close_test_runtime(runtime)
def test_download_file(temp_dir, runtime_cls, run_as_openhands):
def test_download_file(temp_dir, runtime_cls, run_as_openhands, dynamic_port):
"""Test downloading a file using the browser."""
runtime, config = _load_runtime(
temp_dir, runtime_cls, run_as_openhands, enable_browser=True
@@ -1142,7 +1156,7 @@ def test_download_file(temp_dir, runtime_cls, run_as_openhands):
# Start HTTP server
action_cmd = CmdRunAction(
command='python3 -m http.server 8000 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
@@ -1157,19 +1171,19 @@ def test_download_file(temp_dir, runtime_cls, run_as_openhands):
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Browse to the HTML page
action_browse = BrowseURLAction(url='http://localhost:8000/download_test.html')
action_browse = BrowseURLAction(url=f'http://localhost:{dynamic_port}/download_test.html')
logger.info(action_browse, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_browse)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
# Verify the browser observation
assert isinstance(obs, BrowserOutputObservation)
assert 'http://localhost:8000/download_test.html' in obs.url
assert f'http://localhost:{dynamic_port}/download_test.html' in obs.url
assert not obs.error
assert 'Download Test Page' in obs.content
# Go to the PDF file url directly - this should trigger download
file_url = f'http://localhost:8000/{test_file_name}'
file_url = f'http://localhost:{dynamic_port}/{test_file_name}'
action_browse = BrowseInteractiveAction(
browser_actions=f'goto("{file_url}")',
)
+18 -12
View File
@@ -140,7 +140,9 @@ def test_default_activated_tools():
@pytest.mark.skip('This test is flaky')
@pytest.mark.asyncio
async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands):
async def test_fetch_mcp_via_stdio(
temp_dir, runtime_cls, run_as_openhands, dynamic_port
):
mcp_stdio_server_config = MCPStdioServerConfig(
name='fetch', command='uvx', args=['mcp-server-fetch']
)
@@ -154,7 +156,9 @@ async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands):
)
# Test browser server
action_cmd = CmdRunAction(command='python3 -m http.server 8080 > server.log 2>&1 &')
action_cmd = CmdRunAction(
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action_cmd)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
@@ -169,7 +173,9 @@ async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands):
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert obs.exit_code == 0
mcp_action = MCPAction(name='fetch', arguments={'url': 'http://localhost:8080'})
mcp_action = MCPAction(
name='fetch', arguments={'url': f'http://localhost:{dynamic_port}'}
)
obs = await runtime.call_tool_mcp(mcp_action)
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
assert isinstance(obs, MCPObservation), (
@@ -182,7 +188,7 @@ async def test_fetch_mcp_via_stdio(temp_dir, runtime_cls, run_as_openhands):
assert result_json['content'][0]['type'] == 'text'
assert (
result_json['content'][0]['text']
== 'Contents of http://localhost:8080/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
== f'Contents of http://localhost:{dynamic_port}/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
)
runtime.close()
@@ -223,7 +229,7 @@ async def test_filesystem_mcp_via_sse(
@pytest.mark.skip('This test is flaky')
@pytest.mark.asyncio
async def test_both_stdio_and_sse_mcp(
temp_dir, runtime_cls, run_as_openhands, sse_mcp_docker_server
temp_dir, runtime_cls, run_as_openhands, sse_mcp_docker_server, dynamic_port
):
sse_server_info = sse_mcp_docker_server
sse_url = sse_server_info['url']
@@ -259,7 +265,7 @@ async def test_both_stdio_and_sse_mcp(
# ======= Test stdio server =======
# Test browser server
action_cmd_http = CmdRunAction(
command='python3 -m http.server 8080 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd_http, extra={'msg_type': 'ACTION'})
obs_http = runtime.run_action(action_cmd_http)
@@ -280,7 +286,7 @@ async def test_both_stdio_and_sse_mcp(
# And FastMCP Proxy will pre-pend the server name (in this case, `fetch`)
# to the tool name, so the full tool name becomes `fetch_fetch`
name='fetch',
arguments={'url': 'http://localhost:8080'},
arguments={'url': f'http://localhost:{dynamic_port}'},
)
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
@@ -294,7 +300,7 @@ async def test_both_stdio_and_sse_mcp(
assert result_json['content'][0]['type'] == 'text'
assert (
result_json['content'][0]['text']
== 'Contents of http://localhost:8080/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
== f'Contents of http://localhost:{dynamic_port}/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
)
finally:
if runtime:
@@ -305,7 +311,7 @@ async def test_both_stdio_and_sse_mcp(
@pytest.mark.skip('This test is flaky')
@pytest.mark.asyncio
async def test_microagent_and_one_stdio_mcp_in_config(
temp_dir, runtime_cls, run_as_openhands
temp_dir, runtime_cls, run_as_openhands, dynamic_port
):
runtime = None
try:
@@ -350,7 +356,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
# ======= Test the stdio server added by the microagent =======
# Test browser server
action_cmd_http = CmdRunAction(
command='python3 -m http.server 8080 > server.log 2>&1 &'
command=f'python3 -m http.server {dynamic_port} > server.log 2>&1 &'
)
logger.info(action_cmd_http, extra={'msg_type': 'ACTION'})
obs_http = runtime.run_action(action_cmd_http)
@@ -367,7 +373,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
assert obs_cat.exit_code == 0
mcp_action_fetch = MCPAction(
name='fetch_fetch', arguments={'url': 'http://localhost:8080'}
name='fetch_fetch', arguments={'url': f'http://localhost:{dynamic_port}'}
)
obs_fetch = await runtime.call_tool_mcp(mcp_action_fetch)
logger.info(obs_fetch, extra={'msg_type': 'OBSERVATION'})
@@ -381,7 +387,7 @@ async def test_microagent_and_one_stdio_mcp_in_config(
assert result_json['content'][0]['type'] == 'text'
assert (
result_json['content'][0]['text']
== 'Contents of http://localhost:8080/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
== f'Contents of http://localhost:{dynamic_port}/:\n---\n\n* <.downloads/>\n* <server.log>\n\n---'
)
finally:
if runtime: