Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/integration-runner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,11 @@ jobs:
version: latest
python-version: ${{ matrix.python-version }}

- name: Install Chromium
run: |
sudo apt-get update
sudo apt-get install -y chromium-browser

- name: Install Python dependencies using uv
run: |
uv sync --dev
Expand Down
7 changes: 7 additions & 0 deletions openhands-tools/openhands/tools/browser_use/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,13 @@ def close(self):
finally:
# Always close the async executor
self._async_executor.close()
# Release the shared executor reference so the class variable
# doesn't keep a stale reference that could prevent process exit.
from openhands.tools.browser_use.definition import BrowserToolSet

with BrowserToolSet._shared_executor_lock:
if BrowserToolSet._shared_executor is self:
BrowserToolSet._shared_executor = None

def __del__(self):
"""Cleanup on deletion."""
Expand Down
28 changes: 25 additions & 3 deletions tests/integration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,27 @@ def instruction_message(self) -> Message:
return Message(role="user", content=[TextContent(text=self.instruction)])

@property
@abstractmethod
def enable_browser(self) -> bool:
"""Whether to enable browser tools. Override in subclasses that need browsing.

Returns:
False by default. Override to True for tests that require browser access.
"""
return False

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent."""
pass
"""List of tools available to the agent.

By default, uses the configured tool preset with browser support controlled
by the ``enable_browser`` property. This ensures integration tests validate
the same agent configuration shipped to production (GUI/CLI).

Override this property in subclasses that need custom tool configurations.
"""
return get_tools_for_preset(
self.tool_preset, enable_browser=self.enable_browser
)

@property
def condenser(self) -> CondenserBase | None:
Expand Down Expand Up @@ -373,3 +390,8 @@ def teardown(self):
The workspace directory is torn down externally.
Add any additional cleanup (git, server, ...) here if needed.
"""
# Close the conversation to ensure all tool executors (including the
# browser / Chrome process) are shut down. Without this, worker
# processes in ProcessPoolExecutor hang indefinitely because the
# browser's background threads keep them alive.
self.conversation.close()
10 changes: 8 additions & 2 deletions tests/integration/run_infer.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,12 @@ def run_evaluation(
result = process_instance(instance, llm_config, tool_preset)
results.append(result)
else:
# Parallel execution
with ProcessPoolExecutor(max_workers=num_workers) as executor:
# Parallel execution – avoid ProcessPoolExecutor context manager
# because worker processes that spawn browser/Chrome subprocesses
# may not exit cleanly, causing shutdown(wait=True) to hang
# indefinitely.
executor = ProcessPoolExecutor(max_workers=num_workers)
try:
future_to_instance = {
executor.submit(
process_instance, instance, llm_config, tool_preset
Expand All @@ -309,6 +313,8 @@ def run_evaluation(
for future in as_completed(future_to_instance):
result = future.result()
results.append(result)
finally:
executor.shutdown(wait=False, cancel_futures=True)

return results

Expand Down
12 changes: 1 addition & 11 deletions tests/integration/tests/b05_do_not_create_redundant_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@
from textwrap import dedent

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import (
BaseIntegrationTest,
SkipTest,
TestResult,
get_tools_for_preset,
)
from tests.integration.base import BaseIntegrationTest, SkipTest, TestResult
from tests.integration.behavior_utils import (
get_conversation_summary,
)
Expand All @@ -35,10 +29,6 @@ class NoRedundantFilesTest(BaseIntegrationTest):

INSTRUCTION: str = INSTRUCTION

@property
def tools(self) -> list[Tool]:
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None: # noqa: D401
"""Set up a realistic codebase by cloning the lerobot repo."""
try:
Expand Down
8 changes: 1 addition & 7 deletions tests/integration/tests/t01_fix_simple_typo.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import os

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = (
Expand Down Expand Up @@ -32,11 +31,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.document_path: str = os.path.join(self.workspace, "document.txt")

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Create a text file with typos for the agent to fix."""
# Create the test file with typos
Expand Down
8 changes: 1 addition & 7 deletions tests/integration/tests/t02_add_bash_hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import os

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = "Write a shell script 'shell/hello.sh' that prints 'hello'."
Expand All @@ -22,11 +21,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.script_path: str = os.path.join(self.workspace, "shell", "hello.sh")

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Setup is not needed - agent will create directories as needed."""

Expand Down
8 changes: 1 addition & 7 deletions tests/integration/tests/t03_jupyter_write_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import os

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = (
Expand All @@ -25,11 +24,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.file_path: str = os.path.join(self.workspace, "test.txt")

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Setup is not needed - agent will create directories as needed."""

Expand Down
8 changes: 1 addition & 7 deletions tests/integration/tests/t04_git_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import subprocess

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = (
Expand All @@ -21,11 +20,6 @@ class GitStagingTest(BaseIntegrationTest):

INSTRUCTION: str = INSTRUCTION

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Set up git repository with staged changes."""
# Initialize git repository
Expand Down
9 changes: 4 additions & 5 deletions tests/integration/tests/t05_simple_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@

from openhands.sdk import get_logger
from openhands.sdk.conversation import get_agent_final_response
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = "Browse localhost:8000, and tell me the ultimate answer to life."
Expand Down Expand Up @@ -99,9 +98,9 @@ def __init__(self, *args, **kwargs):
self.server_process: subprocess.Popen[bytes] | None = None

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)
def enable_browser(self) -> bool:
"""Enable browser tools for this browsing test."""
return True

def setup(self) -> None:
"""Set up a local web server with the HTML file."""
Expand Down
9 changes: 4 additions & 5 deletions tests/integration/tests/t06_github_pr_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

from openhands.sdk import get_logger
from openhands.sdk.conversation import get_agent_final_response
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = (
Expand All @@ -21,9 +20,9 @@ class GitHubPRBrowsingTest(BaseIntegrationTest):
INSTRUCTION: str = INSTRUCTION

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)
def enable_browser(self) -> bool:
"""Enable browser tools for this browsing test."""
return True

def setup(self) -> None:
"""No special setup needed for GitHub PR browsing."""
Expand Down
8 changes: 1 addition & 7 deletions tests/integration/tests/t07_interactive_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import os

from openhands.sdk import get_logger
from openhands.sdk.tool import Tool
from tests.integration.base import BaseIntegrationTest, TestResult, get_tools_for_preset
from tests.integration.base import BaseIntegrationTest, TestResult


INSTRUCTION = (
Expand Down Expand Up @@ -38,11 +37,6 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.script_path: str = os.path.join(self.workspace, "python_script.py")

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Set up the interactive Python script."""

Expand Down
13 changes: 1 addition & 12 deletions tests/integration/tests/t08_image_file_viewing.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@

from openhands.sdk import get_logger
from openhands.sdk.conversation.response_utils import get_agent_final_response
from openhands.sdk.tool import Tool
from tests.integration.base import (
BaseIntegrationTest,
SkipTest,
TestResult,
get_tools_for_preset,
)
from tests.integration.base import BaseIntegrationTest, SkipTest, TestResult


INSTRUCTION = (
Expand Down Expand Up @@ -41,11 +35,6 @@ def __init__(self, *args, **kwargs):
"Please use a model that supports image input."
)

@property
def tools(self) -> list[Tool]:
"""List of tools available to the agent based on configured tool preset."""
return get_tools_for_preset(self.tool_preset, enable_browser=False)

def setup(self) -> None:
"""Download the OpenHands logo for the agent to analyze."""
try:
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/utils/behavior_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def clone_pinned_software_agent_repo(workspace: str) -> Path:


def default_behavior_tools(tool_preset: ToolPresetType = "default") -> list[Tool]:
"""Register and return tools for behavior tests based on the tool preset."""
"""Return the default tools for behavior tests based on the tool preset."""
return get_tools_for_preset(tool_preset, enable_browser=False)


Expand Down
Loading