mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
uv-migrati
...
fix-runtim
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ab9c7387 | ||
|
|
2fe513410a |
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
18
openhands-cli/uv.lock
generated
18
openhands-cli/uv.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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} &')
|
||||
|
||||
@@ -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}")',
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user