Skip to content

Commit dee2322

Browse files
fix: address review feedback for health_check_timeout
- Add gt=0.0 validator to reject zero/negative timeouts - Remove dead default from _wait_for_health methods (make timeout required) - Fix test_docker_workspace_health_check_timeout_custom to actually verify _wait_for_health is called with custom timeout (blocker) - Add test_docker_workspace_startup_uses_health_check_timeout for _start_container path (significant gap) - Collapse redundant *_field_exists/*_default tests into parametrized tests - Add validation tests for non-positive timeout values Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 9394dc6 commit dee2322

3 files changed

Lines changed: 45 additions & 45 deletions

File tree

openhands-workspace/openhands/workspace/apptainer/workspace.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class ApptainerWorkspace(RemoteWorkspace):
130130
)
131131
health_check_timeout: float = Field(
132132
default=120.0,
133+
gt=0.0,
133134
description="Timeout in seconds to wait for container health check to pass.",
134135
)
135136

@@ -326,7 +327,7 @@ def _stream_logs(self) -> None:
326327
except Exception:
327328
pass
328329

329-
def _wait_for_health(self, timeout: float = 120.0) -> None:
330+
def _wait_for_health(self, *, timeout: float) -> None:
330331
"""Wait for the container to become healthy."""
331332
start = time.time()
332333
health_url = f"http://127.0.0.1:{self.host_port}/health"

openhands-workspace/openhands/workspace/docker/workspace.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class DockerWorkspace(RemoteWorkspace):
123123
)
124124
health_check_timeout: float = Field(
125125
default=120.0,
126+
gt=0.0,
126127
description="Timeout in seconds to wait for container health check to pass.",
127128
)
128129

@@ -313,7 +314,7 @@ def _stream_docker_logs(self) -> None:
313314
except Exception:
314315
pass
315316

316-
def _wait_for_health(self, timeout: float = 120.0) -> None:
317+
def _wait_for_health(self, *, timeout: float) -> None:
317318
"""Wait for the Docker container to become healthy."""
318319
start = time.time()
319320
# We can construct the health URL based on self.host if available,

tests/workspace/test_docker_workspace.py

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from unittest.mock import MagicMock, Mock, patch
88

99
import pytest
10+
from pydantic import ValidationError
1011

1112
from openhands.workspace import (
1213
ApptainerWorkspace,
@@ -177,40 +178,56 @@ def test_docker_network(mock_docker_workspace):
177178
assert run_cmd[network_index + 1] == network_name
178179

179180

180-
def test_docker_workspace_health_check_timeout_field_exists():
181-
"""Test that health_check_timeout is a recognised model field."""
182-
183-
assert "health_check_timeout" in DockerWorkspace.model_fields
181+
# ===========================================================================
182+
# health_check_timeout tests for DockerWorkspace and ApptainerWorkspace
183+
# ===========================================================================
184184

185185

186-
def test_docker_workspace_health_check_timeout_default():
186+
@pytest.mark.parametrize("cls", [DockerWorkspace, ApptainerWorkspace])
187+
def test_health_check_timeout_default(cls):
187188
"""Test that health_check_timeout defaults to 120.0 seconds."""
189+
assert cls.model_fields["health_check_timeout"].default == 120.0
190+
188191

189-
field = DockerWorkspace.model_fields["health_check_timeout"]
190-
assert field.default == 120.0
192+
@pytest.mark.parametrize("cls", [DockerWorkspace, ApptainerWorkspace])
193+
def test_health_check_timeout_rejects_non_positive(cls):
194+
"""Test that health_check_timeout rejects zero and negative values."""
195+
with pytest.raises(ValidationError, match="greater than 0"):
196+
# Attempt to create with invalid timeout - we need to mock startup
197+
with patch.object(cls, "model_post_init"):
198+
cls.model_validate(
199+
{"server_image": "test:latest", "health_check_timeout": 0}
200+
)
191201

202+
with pytest.raises(ValidationError, match="greater than 0"):
203+
with patch.object(cls, "model_post_init"):
204+
cls.model_validate(
205+
{"server_image": "test:latest", "health_check_timeout": -10.0}
206+
)
192207

193-
def test_docker_workspace_health_check_timeout_custom(mock_docker_workspace):
194-
"""Test that a custom health_check_timeout is forwarded to _wait_for_health."""
195208

196-
with patch.object(DockerWorkspace, "_wait_for_health") as mock_wait:
197-
with patch.object(DockerWorkspace, "_start_container"):
198-
with patch(
199-
"openhands.workspace.docker.workspace.execute_command"
200-
) as mock_exec:
201-
mock_exec.return_value = Mock(returncode=0, stdout="", stderr="")
202-
workspace = DockerWorkspace(
203-
server_image="test:latest", health_check_timeout=60.0
204-
)
205-
assert workspace.health_check_timeout == 60.0
206-
# The field is set; _wait_for_health is called by _start_container which
207-
# is mocked, so verify the value round-trips through the model.
208-
_ = mock_wait # referenced to satisfy linters
209+
def test_docker_workspace_startup_uses_health_check_timeout():
210+
"""Test that _start_container passes health_check_timeout to _wait_for_health."""
211+
with (
212+
patch(
213+
"openhands.workspace.docker.workspace.check_port_available",
214+
return_value=True,
215+
),
216+
patch(
217+
"openhands.workspace.docker.workspace.find_available_tcp_port",
218+
return_value=8000,
219+
),
220+
patch("openhands.workspace.docker.workspace.execute_command") as mock_exec,
221+
patch.object(DockerWorkspace, "_wait_for_health") as mock_wait,
222+
patch("openhands.workspace.docker.workspace.RemoteWorkspace.model_post_init"),
223+
):
224+
mock_exec.return_value = Mock(returncode=0, stdout="container_123", stderr="")
225+
DockerWorkspace(server_image="test:latest", health_check_timeout=60.0)
226+
mock_wait.assert_called_once_with(timeout=60.0)
209227

210228

211229
def test_docker_workspace_resume_uses_health_check_timeout(mock_docker_workspace):
212230
"""Test that resume() passes health_check_timeout to _wait_for_health."""
213-
214231
with patch.object(DockerWorkspace, "_start_container"):
215232
with patch("openhands.workspace.docker.workspace.execute_command") as mock_exec:
216233
mock_exec.return_value = Mock(returncode=0, stdout="", stderr="")
@@ -226,27 +243,8 @@ def test_docker_workspace_resume_uses_health_check_timeout(mock_docker_workspace
226243
mock_wait.assert_called_once_with(timeout=30.0)
227244

228245

229-
# ===========================================================================
230-
# Apptainer health_check_timeout tests (co-located for compactness)
231-
# ===========================================================================
232-
233-
234-
def test_apptainer_workspace_health_check_timeout_field_exists():
235-
"""Test that health_check_timeout is a recognised model field."""
236-
237-
assert "health_check_timeout" in ApptainerWorkspace.model_fields
238-
239-
240-
def test_apptainer_workspace_health_check_timeout_default():
241-
"""Test that health_check_timeout defaults to 120.0 seconds."""
242-
243-
field = ApptainerWorkspace.model_fields["health_check_timeout"]
244-
assert field.default == 120.0
245-
246-
247-
def test_apptainer_workspace_health_check_timeout_startup_call():
246+
def test_apptainer_workspace_startup_uses_health_check_timeout():
248247
"""Test that model_post_init passes health_check_timeout to _wait_for_health."""
249-
250248
with (
251249
patch("openhands.workspace.apptainer.workspace.execute_command") as mock_exec,
252250
patch(

0 commit comments

Comments
 (0)