From ee14f1ea416014101b476eecc156d24705c16806 Mon Sep 17 00:00:00 2001 From: Boxuan Li Date: Fri, 18 Jul 2025 11:54:53 -0700 Subject: [PATCH] Remove poetry dependency in Jupyter Plugin (#9789) --- .../{py-unit-tests.yml => py-tests.yml} | 10 ++- openhands/agenthub/dummy_agent/agent.py | 88 +++++++++++-------- openhands/runtime/impl/local/local_runtime.py | 3 +- tests/e2e/test_local_runtime.py | 62 +++++++++++++ 4 files changed, 122 insertions(+), 41 deletions(-) rename .github/workflows/{py-unit-tests.yml => py-tests.yml} (93%) create mode 100644 tests/e2e/test_local_runtime.py diff --git a/.github/workflows/py-unit-tests.yml b/.github/workflows/py-tests.yml similarity index 93% rename from .github/workflows/py-unit-tests.yml rename to .github/workflows/py-tests.yml index c988b8427f..bf8bee17a1 100644 --- a/.github/workflows/py-unit-tests.yml +++ b/.github/workflows/py-tests.yml @@ -1,5 +1,5 @@ -# Workflow that runs python unit tests -name: Run Python Unit Tests +# Workflow that runs python tests +name: Run Python Tests # The jobs in this workflow are required, so they must run at all times # * Always run on "main" @@ -16,9 +16,9 @@ concurrency: cancel-in-progress: true jobs: - # Run python unit tests on Linux + # Run python tests on Linux test-on-linux: - name: Python Unit Tests on Linux + name: Python Tests on Linux runs-on: blacksmith-4vcpu-ubuntu-2204 env: INSTALL_DOCKER: '0' # Set to '0' to skip Docker installation @@ -51,6 +51,8 @@ jobs: run: poetry run pytest --forked -n auto -svv ./tests/unit - name: Run Runtime Tests with CLIRuntime run: TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py + - name: Run E2E Tests + run: poetry run pytest -svv tests/e2e # Run specific Windows python tests test-on-windows: diff --git a/openhands/agenthub/dummy_agent/agent.py b/openhands/agenthub/dummy_agent/agent.py index d173b53529..0d644a60cd 100644 --- a/openhands/agenthub/dummy_agent/agent.py +++ b/openhands/agenthub/dummy_agent/agent.py @@ -8,8 +8,6 @@ from openhands.events.action import ( Action, AgentFinishAction, AgentRejectAction, - BrowseInteractiveAction, - BrowseURLAction, CmdRunAction, FileReadAction, FileWriteAction, @@ -17,7 +15,6 @@ from openhands.events.action import ( ) from openhands.events.observation import ( AgentStateChangedObservation, - BrowserOutputObservation, CmdOutputMetadata, CmdOutputObservation, FileReadObservation, @@ -54,17 +51,19 @@ class DummyAgent(Agent): }, { 'action': CmdRunAction(command='echo "foo"'), - 'observations': [CmdOutputObservation('foo', command='echo "foo"')], + 'observations': [ + CmdOutputObservation( + 'foo', + command='echo "foo"', + metadata=CmdOutputMetadata(exit_code=0), + ) + ], }, { 'action': FileWriteAction( content='echo "Hello, World!"', path='hello.sh' ), - 'observations': [ - FileWriteObservation( - content='echo "Hello, World!"', path='hello.sh' - ) - ], + 'observations': [FileWriteObservation(content='', path='hello.sh')], }, { 'action': FileReadAction(path='hello.sh'), @@ -76,36 +75,12 @@ class DummyAgent(Agent): 'action': CmdRunAction(command='bash hello.sh'), 'observations': [ CmdOutputObservation( - 'bash: hello.sh: No such file or directory', - command='bash workspace/hello.sh', - metadata=CmdOutputMetadata(exit_code=127), + 'Hello, World!', + command='bash hello.sh', + metadata=CmdOutputMetadata(exit_code=0), ) ], }, - { - 'action': BrowseURLAction(url='https://google.com'), - 'observations': [ - BrowserOutputObservation( - 'Simulated Google page', - url='https://google.com', - screenshot='', - trigger_by_action='', - ), - ], - }, - { - 'action': BrowseInteractiveAction( - browser_actions='goto("https://google.com")' - ), - 'observations': [ - BrowserOutputObservation( - 'Simulated Google page after interaction', - url='https://google.com', - screenshot='', - trigger_by_action='', - ), - ], - }, { 'action': AgentRejectAction(), 'observations': [AgentStateChangedObservation('', AgentState.REJECTED)], @@ -147,6 +122,47 @@ class DummyAgent(Agent): obs.pop('timestamp', None) obs.pop('cause', None) obs.pop('source', None) + # Remove dynamic metadata fields that vary between runs + if 'extras' in obs and 'metadata' in obs['extras']: + metadata = obs['extras']['metadata'] + if isinstance(metadata, dict): + metadata.pop('pid', None) + metadata.pop('username', None) + metadata.pop('hostname', None) + metadata.pop('working_dir', None) + metadata.pop('py_interpreter_path', None) + metadata.pop('suffix', None) + # Normalize file paths for comparison - extract just the filename + if 'extras' in obs and 'path' in obs['extras']: + path = obs['extras']['path'] + if isinstance(path, str): + # Extract just the filename from the path + import os + + obs['extras']['path'] = os.path.basename(path) + # Normalize message field to handle path differences + if 'message' in obs: + import os + + message = obs['message'] + if isinstance(message, str): + # Replace full paths with just filenames in messages + if 'I wrote to the file ' in message: + parts = message.split('I wrote to the file ') + if len(parts) == 2: + filename = os.path.basename( + parts[1].rstrip('.') + ) + obs['message'] = ( + f'I wrote to the file {filename}.' + ) + elif 'I read the file ' in message: + parts = message.split('I read the file ') + if len(parts) == 2: + filename = os.path.basename( + parts[1].rstrip('.') + ) + obs['message'] = f'I read the file {filename}.' if hist_obs != expected_obs: print( diff --git a/openhands/runtime/impl/local/local_runtime.py b/openhands/runtime/impl/local/local_runtime.py index 852cc34aa4..0a850794dc 100644 --- a/openhands/runtime/impl/local/local_runtime.py +++ b/openhands/runtime/impl/local/local_runtime.py @@ -635,7 +635,8 @@ def _create_server( server_port=execution_server_port, plugins=plugins, app_config=config, - python_prefix=['poetry', 'run'], + python_prefix=[], + python_executable=sys.executable, override_user_id=user_id, override_username=username, ) diff --git a/tests/e2e/test_local_runtime.py b/tests/e2e/test_local_runtime.py new file mode 100644 index 0000000000..437c2f0060 --- /dev/null +++ b/tests/e2e/test_local_runtime.py @@ -0,0 +1,62 @@ +import os +import shutil +import subprocess +import tempfile + + +def test_headless_mode_with_dummy_agent_no_browser(): + """ + E2E test: build a docker image from python:3.13, install openhands from source, + and run a local runtime task in headless mode. + """ + repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../')) + dockerfile = """ + FROM python:3.13-slim + WORKDIR /src + RUN apt-get update && apt-get install -y git build-essential tmux + COPY . /src + RUN pip install --upgrade pip setuptools wheel + RUN pip install . + ENV PYTHONUNBUFFERED=1 + ENV RUNTIME=local + ENV RUN_AS_OPENHANDS=false + ENV ENABLE_BROWSER=false + ENV AGENT_ENABLE_BROWSING=false + ENV SKIP_DEPENDENCY_CHECK=1 + CMD ["python", "-m", "openhands.core.main", "-c", "DummyAgent", "-t", "Hello world"] + """ + + with tempfile.TemporaryDirectory() as tmpdir: + dockerfile_path = os.path.join(tmpdir, 'Dockerfile') + with open(dockerfile_path, 'w') as f: + f.write(dockerfile) + # Copy the repo into the temp dir for docker build context + build_context = os.path.join(tmpdir, 'context') + shutil.copytree(repo_root, build_context, dirs_exist_ok=True) + + image_tag = 'openhands-e2e-local-runtime-test' + build_cmd = [ + 'docker', + 'build', + '-t', + image_tag, + '-f', + dockerfile_path, + build_context, + ] + run_cmd = ['docker', 'run', '--rm', image_tag] + + # Build the image + build_proc = subprocess.run(build_cmd, capture_output=True, text=True) + print('Docker build stdout:', build_proc.stdout) + print('Docker build stderr:', build_proc.stderr) + assert build_proc.returncode == 0, 'Docker build failed' + + # Run the container + run_proc = subprocess.run(run_cmd, capture_output=True, text=True) + print('Docker run stdout:', run_proc.stdout) + print('Docker run stderr:', run_proc.stderr) + assert run_proc.returncode == 0, ( + f'Docker run failed with code {run_proc.returncode}' + ) + assert 'Warning: Observation mismatch' not in run_proc.stdout