77from unittest .mock import MagicMock , Mock , patch
88
99import pytest
10+ from pydantic import ValidationError
1011
1112from 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
211229def 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