@@ -294,6 +294,129 @@ def test_create_without_spec_template_or_snapshot_raises(self, mock_create):
294294
295295 mock_create .assert_not_called ()
296296
297+ @staticmethod
298+ def _exec_response (payload ):
299+ """Builds the response the sandbox proxy returns for POST /exec."""
300+ return agentplatform_types .ExecuteSandboxEnvironmentResponse (
301+ outputs = [
302+ agentplatform_types .Chunk (
303+ mime_type = "application/json" ,
304+ data = json .dumps (payload ).encode ("utf-8" ),
305+ )
306+ ]
307+ )
308+
309+ @staticmethod
310+ def _sent_chunks (mock_execute_code ):
311+ """Returns the uri, port and JSON body sent to the sandbox."""
312+ _ , kwargs = mock_execute_code .call_args
313+ chunks = {chunk .mime_type : chunk .data for chunk in kwargs ["inputs" ]}
314+ return (
315+ chunks ["application/x.sandbox-request-uri" ],
316+ chunks ["application/x.sandbox-request-port" ],
317+ json .loads (chunks ["application/json" ]),
318+ )
319+
320+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
321+ def test_execute_bash_runs_command_and_parses_result (self , mock_execute_code ):
322+ """A command is sent to /exec and its JSON result is returned."""
323+ expected = {
324+ "stdout" : "hello\n " ,
325+ "stderr" : "" ,
326+ "returncode" : 0 ,
327+ "duration_ms" : 5 ,
328+ }
329+ mock_execute_code .return_value = self ._exec_response (expected )
330+
331+ result = self .client .sandboxes .execute_bash (
332+ name = _TEST_SANDBOX_RESOURCE_NAME ,
333+ command = "echo hello" ,
334+ )
335+
336+ assert result == expected
337+ _ , kwargs = mock_execute_code .call_args
338+ assert kwargs ["name" ] == _TEST_SANDBOX_RESOURCE_NAME
339+ uri , port , body = self ._sent_chunks (mock_execute_code )
340+ assert uri == b"/exec"
341+ assert port == b"8080"
342+ # cwd and timeout are left to the sandbox's own defaults when unset.
343+ assert body == {"command" : "echo hello" }
344+
345+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
346+ def test_execute_bash_forwards_cwd_and_timeout (self , mock_execute_code ):
347+ """cwd and timeout reach the container when the caller sets them."""
348+ mock_execute_code .return_value = self ._exec_response ({"stdout" : "/tmp\n " })
349+
350+ self .client .sandboxes .execute_bash (
351+ name = _TEST_SANDBOX_RESOURCE_NAME ,
352+ command = "pwd" ,
353+ cwd = "/tmp" ,
354+ timeout = 30 ,
355+ )
356+
357+ _ , _ , body = self ._sent_chunks (mock_execute_code )
358+ assert body == {"command" : "pwd" , "cwd" : "/tmp" , "timeout" : 30 }
359+
360+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
361+ def test_execute_bash_uses_custom_port (self , mock_execute_code ):
362+ """A caller-supplied port overrides the shell server default."""
363+ mock_execute_code .return_value = self ._exec_response ({"stdout" : "" })
364+
365+ self .client .sandboxes .execute_bash (
366+ name = _TEST_SANDBOX_RESOURCE_NAME ,
367+ command = "true" ,
368+ port = "9222" ,
369+ )
370+
371+ _ , port , _ = self ._sent_chunks (mock_execute_code )
372+ assert port == b"9222"
373+
374+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
375+ def test_execute_bash_reports_failure_without_raising (self , mock_execute_code ):
376+ """A non-zero exit is returned on the result, not raised."""
377+ expected = {
378+ "stdout" : "" ,
379+ "stderr" : "ls: cannot access '/nope': No such file or directory\n " ,
380+ "returncode" : 2 ,
381+ "duration_ms" : 4 ,
382+ }
383+ mock_execute_code .return_value = self ._exec_response (expected )
384+
385+ result = self .client .sandboxes .execute_bash (
386+ name = _TEST_SANDBOX_RESOURCE_NAME ,
387+ command = "ls /nope" ,
388+ )
389+
390+ assert result == expected
391+
392+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
393+ def test_execute_bash_forwards_config (self , mock_execute_code ):
394+ """The request config is passed through untouched."""
395+ mock_execute_code .return_value = self ._exec_response ({"stdout" : "" })
396+ config = agentplatform_types .ExecuteCodeRuntimeSandboxConfig ()
397+
398+ self .client .sandboxes .execute_bash (
399+ name = _TEST_SANDBOX_RESOURCE_NAME ,
400+ command = "true" ,
401+ config = config ,
402+ )
403+
404+ _ , kwargs = mock_execute_code .call_args
405+ assert kwargs ["config" ] is config
406+
407+ @mock .patch .object (sandboxes .Sandboxes , "_execute_code" )
408+ def test_execute_bash_without_output_raises (self , mock_execute_code ):
409+ """An empty response is an error, not a silent success."""
410+ mock_execute_code .return_value = (
411+ agentplatform_types .ExecuteSandboxEnvironmentResponse (outputs = [])
412+ )
413+
414+ with pytest .raises (ValueError , match = "returned no output" ):
415+ self .client .sandboxes .execute_bash (
416+ name = _TEST_SANDBOX_RESOURCE_NAME ,
417+ command = "echo hello" ,
418+ )
419+
297420
298421@pytest .mark .usefixtures ("google_auth_mock" )
299422class TestSandboxTemplates :
0 commit comments