chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,13 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
@@ -0,0 +1,502 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
from unittest.mock import MagicMock
from unittest.mock import patch
from google.adk.agents.invocation_context import InvocationContext
from google.adk.code_executors.agent_engine_sandbox_code_executor import AgentEngineSandboxCodeExecutor
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
from google.adk.code_executors.code_execution_utils import File
from google.adk.sessions.session import Session
import pytest
@pytest.fixture
def mock_invocation_context() -> InvocationContext:
"""Fixture for a mock InvocationContext."""
mock = MagicMock(spec=InvocationContext)
mock.invocation_id = "test-invocation-123"
session = MagicMock(spec=Session)
mock.session = session
session.state = {}
return mock
class TestAgentEngineSandboxCodeExecutor:
"""Unit tests for the AgentEngineSandboxCodeExecutor."""
def test_init_with_sandbox_overrides(self):
"""Tests that class attributes can be overridden at instantiation."""
executor = AgentEngineSandboxCodeExecutor(
sandbox_resource_name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789",
)
assert executor.sandbox_resource_name == (
"projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
)
def test_init_with_sandbox_overrides_throws_error(self):
"""Tests that class attributes can be overridden at instantiation."""
with pytest.raises(ValueError):
AgentEngineSandboxCodeExecutor(
sandbox_resource_name="projects/123/locations/us-central1/reasoningEngines/456/sandboxes/789",
)
def test_init_with_agent_engine_overrides_throws_error(self):
"""Tests that class attributes can be overridden at instantiation."""
with pytest.raises(ValueError):
AgentEngineSandboxCodeExecutor(
agent_engine_resource_name=(
"projects/123/locations/us-central1/reason/456"
),
)
@patch("vertexai.Client")
def test_execute_code_success(
self,
mock_vertexai_client,
mock_invocation_context,
):
# Setup Mocks
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps(
{"msg_out": "hello world", "msg_err": ""}
).encode("utf-8")
mock_json_output.metadata = None
mock_file_output = MagicMock()
mock_file_output.mime_type = "text/plain"
mock_file_output.data = b"file content"
mock_file_output.metadata = MagicMock()
mock_file_output.metadata.attributes = {"file_name": b"file.txt"}
mock_png_file_output = MagicMock()
mock_png_file_output.mime_type = "image/png"
sample_png_bytes = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
mock_png_file_output.data = sample_png_bytes
mock_png_file_output.metadata = MagicMock()
mock_png_file_output.metadata.attributes = {"file_name": b"file.png"}
mock_response.outputs = [
mock_json_output,
mock_file_output,
mock_png_file_output,
]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
# Execute
executor = AgentEngineSandboxCodeExecutor(
sandbox_resource_name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
)
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
# Assert
assert result.stdout == "hello world"
assert not result.stderr
assert result.output_files[0].mime_type == "text/plain"
assert result.output_files[0].content == b"file content"
assert result.output_files[0].name == "file.txt"
assert result.output_files[1].mime_type == "image/png"
assert result.output_files[1].name == "file.png"
assert result.output_files[1].content == sample_png_bytes
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789",
input_data={"code": 'print("hello world")'},
)
@patch("vertexai.Client")
def test_execute_code_sends_input_files_with_content_key(
self,
mock_vertexai_client,
mock_invocation_context,
):
"""Input files must be sent under the 'content' key the SDK expects."""
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
mock_response = MagicMock()
mock_response.outputs = []
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
executor = AgentEngineSandboxCodeExecutor(
sandbox_resource_name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
)
code_input = CodeExecutionInput(
code='print("hi")',
input_files=[
File(name="data.csv", content="a,b,c", mime_type="text/csv")
],
)
executor.execute_code(mock_invocation_context, code_input)
_, call_kwargs = (
mock_api_client.agent_engines.sandboxes.execute_code.call_args
)
sent_files = call_kwargs["input_data"]["files"]
assert sent_files == [
{"name": "data.csv", "content": "a,b,c", "mime_type": "text/csv"}
]
@patch("vertexai.Client")
def test_execute_code_recreates_sandbox_when_get_returns_none(
self,
mock_vertexai_client,
mock_invocation_context,
):
# Setup Mocks
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
# Existing sandbox name stored in session, but get() will return None
existing_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/old"
mock_invocation_context.session.state = {
"sandbox_name": existing_sandbox_name
}
# Mock get to return None (simulating missing/expired sandbox)
mock_api_client.agent_engines.sandboxes.get.return_value = None
# Mock create operation to return a new sandbox resource name
operation_mock = MagicMock()
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
operation_mock.response.name = created_sandbox_name
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
# Mock execute_code response
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps(
{"msg_out": "recreated sandbox run", "msg_err": ""}
).encode("utf-8")
mock_json_output.metadata = None
mock_response.outputs = [mock_json_output]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
# Execute using agent_engine_resource_name so a sandbox can be created
executor = AgentEngineSandboxCodeExecutor(
agent_engine_resource_name=(
"projects/123/locations/us-central1/reasoningEngines/456"
)
)
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
# Assert get was called for the existing sandbox
mock_api_client.agent_engines.sandboxes.get.assert_called_once_with(
name=existing_sandbox_name
)
# Assert create was called and session updated with new sandbox
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
assert (
mock_invocation_context.session.state["sandbox_name"]
== created_sandbox_name
)
# Assert execute_code used the created sandbox name
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
name=created_sandbox_name,
input_data={"code": 'print("hello world")'},
)
@patch("vertexai.Client")
def test_execute_code_recreates_sandbox_when_get_raises_client_error(
self,
mock_vertexai_client,
mock_invocation_context,
):
# Setup Mocks
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
# Existing sandbox name stored in session
existing_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/old"
mock_invocation_context.session.state = {
"sandbox_name": existing_sandbox_name
}
# Mock get to raise ClientError with code 404
from google.genai.errors import ClientError
mock_api_client.agent_engines.sandboxes.get.side_effect = ClientError(
code=404, response_json={"message": "Not Found"}
)
# Mock create operation to return a new sandbox resource name
operation_mock = MagicMock()
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
operation_mock.response.name = created_sandbox_name
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
# Mock execute_code response
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps(
{"msg_out": "recreated sandbox run", "msg_err": ""}
).encode("utf-8")
mock_json_output.metadata = None
mock_response.outputs = [mock_json_output]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
# Execute using agent_engine_resource_name so a sandbox can be created
executor = AgentEngineSandboxCodeExecutor(
agent_engine_resource_name=(
"projects/123/locations/us-central1/reasoningEngines/456"
)
)
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
# Assert get was called for the existing sandbox
mock_api_client.agent_engines.sandboxes.get.assert_called_once_with(
name=existing_sandbox_name
)
# Assert create was called and session updated with new sandbox
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
assert (
mock_invocation_context.session.state["sandbox_name"]
== created_sandbox_name
)
# Assert execute_code used the created sandbox name
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
name=created_sandbox_name,
input_data={"code": 'print("hello world")'},
)
@patch("vertexai.Client")
def test_execute_code_creates_sandbox_if_missing(
self,
mock_vertexai_client,
mock_invocation_context,
):
# Setup Mocks
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
# Mock create operation to return a sandbox resource name
operation_mock = MagicMock()
created_sandbox_name = "projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
operation_mock.response.name = created_sandbox_name
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
# Mock execute_code response
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps(
{"stdout": "created sandbox run", "stderr": ""}
).encode("utf-8")
mock_json_output.metadata = None
mock_response.outputs = [mock_json_output]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
# Ensure session.state behaves like a dict for storing sandbox_name
mock_invocation_context.session.state = {}
# Execute using agent_engine_resource_name so a sandbox will be created
executor = AgentEngineSandboxCodeExecutor(
agent_engine_resource_name=(
"projects/123/locations/us-central1/reasoningEngines/456"
),
sandbox_resource_name=None,
)
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
# Assert sandbox creation was called and session state updated
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
create_call_kwargs = (
mock_api_client.agent_engines.sandboxes.create.call_args.kwargs
)
assert create_call_kwargs["name"] == (
"projects/123/locations/us-central1/reasoningEngines/456"
)
assert (
mock_invocation_context.session.state["sandbox_name"]
== created_sandbox_name
)
# Assert execute_code used the created sandbox name
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
name=created_sandbox_name,
input_data={"code": 'print("hello world")'},
)
@patch("vertexai.Client")
def test_execute_code_sends_correct_field_names_for_input_files(
self,
mock_vertexai_client,
mock_invocation_context,
):
"""Input files are sent with 'content' and 'mime_type' keys (not 'contents'/'mimeType')."""
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps({"msg_out": "", "msg_err": ""}).encode(
"utf-8"
)
mock_json_output.metadata = None
mock_response.outputs = [mock_json_output]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
executor = AgentEngineSandboxCodeExecutor(
sandbox_resource_name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789"
)
code_input = CodeExecutionInput(
code="import pandas as pd; df = pd.read_csv('data.csv')",
input_files=[
File(
name="data.csv", content=b"col1,col2\n1,2", mime_type="text/csv"
)
],
)
executor.execute_code(mock_invocation_context, code_input)
mock_api_client.agent_engines.sandboxes.execute_code.assert_called_once_with(
name="projects/123/locations/us-central1/reasoningEngines/456/sandboxEnvironments/789",
input_data={
"code": "import pandas as pd; df = pd.read_csv('data.csv')",
"files": [{
"name": "data.csv",
"content": b"col1,col2\n1,2",
"mime_type": "text/csv",
}],
},
)
def test_init_with_agent_engine_resource_name(self):
"""Tests init when only agent_engine_resource_name is provided."""
agent_engine_name = (
"projects/123/locations/us-central1/reasoningEngines/456"
)
executor = AgentEngineSandboxCodeExecutor(
agent_engine_resource_name=agent_engine_name
)
# Verify the engine name is set, and sandbox remains None.
assert executor.agent_engine_resource_name == agent_engine_name
assert executor.sandbox_resource_name is None
assert executor._project_id == "123"
assert executor._location == "us-central1"
@patch("vertexai.Client")
@patch.dict(
os.environ,
{
"GOOGLE_CLOUD_PROJECT": "test-project-456",
"GOOGLE_CLOUD_LOCATION": "us-central1",
},
)
def test_execute_code_with_auto_create_agent_engine(
self, mock_vertexai_client, mock_invocation_context
):
"""Tests that Agent Engine is created lazily in execute_code."""
# Setup Mocks
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
# Mock Engine Creation
mock_created_engine = MagicMock()
mock_created_engine.api_resource.name = "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1"
mock_api_client.agent_engines.create.return_value = mock_created_engine
# Mock create operation to return a sandbox resource name
operation_mock = MagicMock()
created_sandbox_name = "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1/sandboxEnvironments/789"
operation_mock.response.name = created_sandbox_name
mock_api_client.agent_engines.sandboxes.create.return_value = operation_mock
# Mock execute_code response
mock_response = MagicMock()
mock_json_output = MagicMock()
mock_json_output.mime_type = "application/json"
mock_json_output.data = json.dumps(
{"stdout": "created sandbox run", "stderr": ""}
).encode("utf-8")
mock_json_output.metadata = None
mock_response.outputs = [mock_json_output]
mock_api_client.agent_engines.sandboxes.execute_code.return_value = (
mock_response
)
# Execute
executor = AgentEngineSandboxCodeExecutor()
code_input = CodeExecutionInput(code='print("hello world")')
executor.execute_code(mock_invocation_context, code_input)
# Assert
mock_api_client.agent_engines.create.assert_called_once()
assert (
executor.agent_engine_resource_name
== "projects/test-project-456/locations/us-central1/reasoningEngines/auto-created-ae-1"
)
assert executor.sandbox_resource_name is None
mock_api_client.agent_engines.sandboxes.create.assert_called_once()
assert (
mock_invocation_context.session.state["sandbox_name"]
== created_sandbox_name
)
@patch("vertexai.Client")
@patch.dict(
os.environ,
{
"GOOGLE_CLOUD_PROJECT": "test-project-456",
"GOOGLE_CLOUD_LOCATION": "us-central1",
},
)
def test_execute_code_auto_create_agent_engine_fails(
self, mock_vertexai_client, mock_invocation_context
):
"""Tests error handling when auto-creating Agent Engine fails."""
mock_api_client = MagicMock()
mock_vertexai_client.return_value = mock_api_client
mock_api_client.agent_engines.create.side_effect = Exception(
"Failed to auto-create Agent Engine"
)
executor = AgentEngineSandboxCodeExecutor()
code_input = CodeExecutionInput(code='print("hello world")')
with pytest.raises(Exception, match="Failed to auto-create Agent Engine"):
executor.execute_code(mock_invocation_context, code_input)
@@ -0,0 +1,125 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor
from google.adk.models.llm_request import LlmRequest
from google.genai import types
import pytest
@pytest.fixture
def built_in_executor() -> BuiltInCodeExecutor:
return BuiltInCodeExecutor()
def test_process_llm_request_gemini_2_model_config_none(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests processing when llm_request.config is None for Gemini 2."""
llm_request = LlmRequest(model="gemini-2.5-flash")
built_in_executor.process_llm_request(llm_request)
assert llm_request.config is not None
assert llm_request.config.tools == [
types.Tool(code_execution=types.ToolCodeExecution())
]
def test_process_llm_request_gemini_2_model_tools_none(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests processing when llm_request.config.tools is None for Gemini 2."""
llm_request = LlmRequest(
model="gemini-2.5-pro", config=types.GenerateContentConfig()
)
built_in_executor.process_llm_request(llm_request)
assert llm_request.config.tools == [
types.Tool(code_execution=types.ToolCodeExecution())
]
def test_process_llm_request_gemini_2_model_tools_empty(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests processing when llm_request.config.tools is empty for Gemini 2."""
llm_request = LlmRequest(
model="gemini-2.5-pro",
config=types.GenerateContentConfig(tools=[]),
)
built_in_executor.process_llm_request(llm_request)
assert llm_request.config.tools == [
types.Tool(code_execution=types.ToolCodeExecution())
]
def test_process_llm_request_gemini_2_model_with_existing_tools(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests processing when llm_request.config.tools already has tools for Gemini 2."""
existing_tool = types.Tool(
function_declarations=[
types.FunctionDeclaration(name="test_func", description="A test func")
]
)
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(tools=[existing_tool]),
)
built_in_executor.process_llm_request(llm_request)
assert len(llm_request.config.tools) == 2
assert existing_tool in llm_request.config.tools
assert (
types.Tool(code_execution=types.ToolCodeExecution())
in llm_request.config.tools
)
def test_process_llm_request_non_gemini_2_model(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests that a ValueError is raised for non-Gemini 2 models."""
llm_request = LlmRequest(model="gemini-1.5-flash")
with pytest.raises(ValueError) as excinfo:
built_in_executor.process_llm_request(llm_request)
assert (
"Gemini code execution tool is not supported for model gemini-1.5-flash"
in str(excinfo.value)
)
def test_process_llm_request_non_gemini_2_model_with_disabled_check(
built_in_executor: BuiltInCodeExecutor,
monkeypatch,
):
"""Tests non-Gemini models pass when model-id check is disabled."""
monkeypatch.setenv("ADK_DISABLE_GEMINI_MODEL_ID_CHECK", "true")
llm_request = LlmRequest(model="internal-model-v1")
built_in_executor.process_llm_request(llm_request)
assert llm_request.config is not None
assert llm_request.config.tools == [
types.Tool(code_execution=types.ToolCodeExecution())
]
def test_process_llm_request_no_model_name(
built_in_executor: BuiltInCodeExecutor,
):
"""Tests that a ValueError is raised if model name is not set."""
llm_request = LlmRequest() # Model name defaults to None
with pytest.raises(ValueError) as excinfo:
built_in_executor.process_llm_request(llm_request)
assert "Gemini code execution tool is not supported for model None" in str(
excinfo.value
)
@@ -0,0 +1,179 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import signal
from google.adk.code_executors import code_execution_utils
from google.genai import types
def test_extract_code_and_truncate_content_basic():
"""Tests basic code extraction and content truncation."""
content = types.Content(
role="model",
parts=[
types.Part(
text=(
"Here is some code:\n```python\nx = 1\n```\nAnd some text"
" after."
)
)
],
)
delimiters = [("```python\n", "\n```")]
code = (
code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
)
assert code == "x = 1"
assert len(content.parts) == 2
assert content.parts[0].text == "Here is some code:\n"
assert content.parts[1].executable_code.code == "x = 1"
def test_extract_code_and_truncate_content_multiple_blocks():
"""Tests that the first code block is extracted when multiple exist."""
content = types.Content(
role="model",
parts=[
types.Part(
text=(
"First:\n"
"```python\n"
"x = 1\n"
"```\n"
"Second:\n"
"```python\n"
"y = 2\n"
"```"
)
)
],
)
delimiters = [("```python\n", "\n```")]
code = (
code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
)
assert code == "x = 1"
assert len(content.parts) == 2
assert content.parts[0].text == "First:\n"
assert content.parts[1].executable_code.code == "x = 1"
def test_extract_code_and_truncate_content_no_delimiter():
"""Tests when no delimiters are found in the content."""
content = types.Content(
role="model",
parts=[types.Part(text="Just plain text without code.")],
)
delimiters = [("```python\n", "\n```")]
code = (
code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
)
assert code is None
# Content should be unmodified.
assert len(content.parts) == 1
assert content.parts[0].text == "Just plain text without code."
def test_extract_code_and_truncate_content_redos_vulnerability():
"""Tests that a string that would cause ReDoS behaves reasonably."""
# Construct a long string that contains repeating patterns without matching delimiters.
# The old regex pattern would backtrack exponentially.
ticks = "`" * 3
long_invalid_payload = ticks + "python\n" + "x = 1\n" * 5000 + "not_matching"
content = types.Content(
role="model",
parts=[types.Part(text=long_invalid_payload)],
)
delimiters = [(ticks + "python\n", "\n" + ticks)]
def handler(_signum, _frame):
raise TimeoutError("Test timed out (possible ReDoS regression)")
signal.signal(signal.SIGALRM, handler)
signal.alarm(2)
try:
# If ReDoS vulnerability exists, this call will hang or take a very long time.
code = code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
finally:
signal.alarm(0)
assert code is None
def test_extract_code_and_truncate_content_multiple_delimiter_pairs():
"""Tests code extraction when multiple different delimiter pairs are provided."""
ticks = "`" * 3
# Case 1: First delimiter pair matches first
content = types.Content(
role="model",
parts=[
types.Part(
text="Here is tool code:\n"
+ ticks
+ "tool_code\nx = 1\n"
+ ticks
+ "\nAnd python code:\n"
+ ticks
+ "python\ny = 2\n"
+ ticks
)
],
)
delimiters = [
(ticks + "tool_code\n", "\n" + ticks),
(ticks + "python\n", "\n" + ticks),
]
code = (
code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
)
assert code == "x = 1"
assert len(content.parts) == 2
assert content.parts[0].text == "Here is tool code:\n"
assert content.parts[1].executable_code.code == "x = 1"
# Case 2: Second delimiter pair matches first
content = types.Content(
role="model",
parts=[
types.Part(
text="Here is python code:\n"
+ ticks
+ "python\ny = 2\n"
+ ticks
+ "\nAnd tool code:\n"
+ ticks
+ "tool_code\nx = 1\n"
+ ticks
)
],
)
code = (
code_execution_utils.CodeExecutionUtils.extract_code_and_truncate_content(
content, delimiters
)
)
assert code == "y = 2"
assert len(content.parts) == 2
assert content.parts[0].text == "Here is python code:\n"
assert content.parts[1].executable_code.code == "y = 2"
@@ -0,0 +1,277 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk.code_executors.code_execution_utils import File
from google.adk.code_executors.code_executor_context import CodeExecutorContext
from google.adk.sessions.state import State
import pytest
@pytest.fixture
def empty_state() -> State:
"""Fixture for an empty session state."""
return State({}, {})
@pytest.fixture
def context_with_data() -> CodeExecutorContext:
"""Fixture for a CodeExecutorContext with some prepopulated data."""
state_data = {
"_code_execution_context": {
"execution_session_id": "session123",
"processed_input_files": ["file1.csv", "file2.txt"],
},
"_code_executor_input_files": [
{"name": "input1.txt", "content": "YQ==", "mime_type": "text/plain"}
],
"_code_executor_error_counts": {"invocationA": 2},
}
state = State(state_data, {})
return CodeExecutorContext(state)
def test_init_empty_state(empty_state: State):
"""Test initialization with an empty state."""
ctx = CodeExecutorContext(empty_state)
assert ctx._context == {}
assert ctx._session_state is empty_state
def test_get_state_delta_empty(empty_state: State):
"""Test get_state_delta when context is empty."""
ctx = CodeExecutorContext(empty_state)
delta = ctx.get_state_delta()
assert delta == {"_code_execution_context": {}}
def test_get_state_delta_with_data(context_with_data: CodeExecutorContext):
"""Test get_state_delta with existing context data."""
delta = context_with_data.get_state_delta()
expected_context = {
"execution_session_id": "session123",
"processed_input_files": ["file1.csv", "file2.txt"],
}
assert delta == {"_code_execution_context": expected_context}
def test_get_execution_id_exists(context_with_data: CodeExecutorContext):
"""Test getting an existing execution ID."""
assert context_with_data.get_execution_id() == "session123"
def test_get_execution_id_not_exists(empty_state: State):
"""Test getting execution ID when it doesn't exist."""
ctx = CodeExecutorContext(empty_state)
assert ctx.get_execution_id() is None
def test_set_execution_id(empty_state: State):
"""Test setting an execution ID."""
ctx = CodeExecutorContext(empty_state)
ctx.set_execution_id("new_session_id")
assert ctx._context["execution_session_id"] == "new_session_id"
assert ctx.get_execution_id() == "new_session_id"
def test_get_processed_file_names_exists(
context_with_data: CodeExecutorContext,
):
"""Test getting existing processed file names."""
assert context_with_data.get_processed_file_names() == [
"file1.csv",
"file2.txt",
]
def test_get_processed_file_names_not_exists(empty_state: State):
"""Test getting processed file names when none exist."""
ctx = CodeExecutorContext(empty_state)
assert ctx.get_processed_file_names() == []
def test_add_processed_file_names_new(empty_state: State):
"""Test adding processed file names to an empty context."""
ctx = CodeExecutorContext(empty_state)
ctx.add_processed_file_names(["new_file.py"])
assert ctx._context["processed_input_files"] == ["new_file.py"]
def test_add_processed_file_names_append(
context_with_data: CodeExecutorContext,
):
"""Test appending to existing processed file names."""
context_with_data.add_processed_file_names(["another_file.md"])
assert context_with_data.get_processed_file_names() == [
"file1.csv",
"file2.txt",
"another_file.md",
]
def test_get_input_files_exists(context_with_data: CodeExecutorContext):
"""Test getting existing input files."""
files = context_with_data.get_input_files()
assert len(files) == 1
assert files[0].name == "input1.txt"
assert files[0].content == "YQ=="
assert files[0].mime_type == "text/plain"
def test_get_input_files_not_exists(empty_state: State):
"""Test getting input files when none exist."""
ctx = CodeExecutorContext(empty_state)
assert ctx.get_input_files() == []
def test_add_input_files_new(empty_state: State):
"""Test adding input files to an empty session state."""
ctx = CodeExecutorContext(empty_state)
new_files = [
File(name="new.dat", content="Yg==", mime_type="application/octet-stream")
]
ctx.add_input_files(new_files)
assert empty_state["_code_executor_input_files"] == [{
"name": "new.dat",
"content": "Yg==",
"mime_type": "application/octet-stream",
}]
def test_add_input_files_append(context_with_data: CodeExecutorContext):
"""Test appending to existing input files."""
new_file = File(name="input2.log", content="Yw==", mime_type="text/x-log")
context_with_data.add_input_files([new_file])
expected_files_data = [
{"name": "input1.txt", "content": "YQ==", "mime_type": "text/plain"},
{"name": "input2.log", "content": "Yw==", "mime_type": "text/x-log"},
]
assert (
context_with_data._session_state["_code_executor_input_files"]
== expected_files_data
)
def test_clear_input_files(context_with_data: CodeExecutorContext):
"""Test clearing input files and processed file names."""
context_with_data.clear_input_files()
assert context_with_data._session_state["_code_executor_input_files"] == []
assert context_with_data._context["processed_input_files"] == []
def test_clear_input_files_when_not_exist(empty_state: State):
"""Test clearing input files when they don't exist initially."""
ctx = CodeExecutorContext(empty_state)
ctx.clear_input_files() # Should not raise error
assert "_code_executor_input_files" not in empty_state # Or assert it's empty
assert "_code_execution_context" not in empty_state or not empty_state[
"_code_execution_context"
].get("processed_input_files")
def test_get_error_count_exists(context_with_data: CodeExecutorContext):
"""Test getting an existing error count."""
assert context_with_data.get_error_count("invocationA") == 2
def test_get_error_count_invocation_not_exists(
context_with_data: CodeExecutorContext,
):
"""Test getting error count for an unknown invocation ID."""
assert context_with_data.get_error_count("invocationB") == 0
def test_get_error_count_no_error_key(empty_state: State):
"""Test getting error count when the error key itself doesn't exist."""
ctx = CodeExecutorContext(empty_state)
assert ctx.get_error_count("any_invocation") == 0
def test_increment_error_count_new_invocation(empty_state: State):
"""Test incrementing error count for a new invocation ID."""
ctx = CodeExecutorContext(empty_state)
ctx.increment_error_count("invocationNew")
assert empty_state["_code_executor_error_counts"]["invocationNew"] == 1
def test_increment_error_count_existing_invocation(
context_with_data: CodeExecutorContext,
):
"""Test incrementing error count for an existing invocation ID."""
context_with_data.increment_error_count("invocationA")
assert (
context_with_data._session_state["_code_executor_error_counts"][
"invocationA"
]
== 3
)
def test_reset_error_count_exists(context_with_data: CodeExecutorContext):
"""Test resetting an existing error count."""
context_with_data.reset_error_count("invocationA")
assert "invocationA" not in (
context_with_data._session_state["_code_executor_error_counts"]
)
def test_reset_error_count_not_exists(context_with_data: CodeExecutorContext):
"""Test resetting an error count that doesn't exist."""
context_with_data.reset_error_count("invocationB") # Should not raise
assert "invocationB" not in (
context_with_data._session_state["_code_executor_error_counts"]
)
def test_reset_error_count_no_error_key(empty_state: State):
"""Test resetting when the error key itself doesn't exist."""
ctx = CodeExecutorContext(empty_state)
ctx.reset_error_count("any_invocation") # Should not raise
assert "_code_executor_error_counts" not in empty_state
def test_update_code_execution_result_new_invocation(empty_state: State):
"""Test updating code execution result for a new invocation."""
ctx = CodeExecutorContext(empty_state)
ctx.update_code_execution_result("inv1", "print('hi')", "hi", "")
results = empty_state["_code_execution_results"]["inv1"]
assert len(results) == 1
assert results[0]["code"] == "print('hi')"
assert results[0]["result_stdout"] == "hi"
assert results[0]["result_stderr"] == ""
assert "timestamp" in results[0]
def test_update_code_execution_result_append(
context_with_data: CodeExecutorContext,
):
"""Test appending to existing code execution results for an invocation."""
# First, let's add an initial result for a new invocation to the existing state
context_with_data._session_state["_code_execution_results"] = {
"invocationX": [{
"code": "old_code",
"result_stdout": "old_out",
"result_stderr": "old_err",
"timestamp": 123,
}]
}
context_with_data.update_code_execution_result(
"invocationX", "new_code", "new_out", "new_err"
)
results = context_with_data._session_state["_code_execution_results"][
"invocationX"
]
assert len(results) == 2
assert results[1]["code"] == "new_code"
assert results[1]["result_stdout"] == "new_out"
assert results[1]["result_stderr"] == "new_err"
@@ -0,0 +1,58 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for the ContainerCodeExecutor container hardening defaults."""
from unittest import mock
from google.adk.code_executors.container_code_executor import ContainerCodeExecutor
def _mock_docker_client():
"""Returns a mock Docker client whose container passes python verification."""
client = mock.MagicMock()
container = mock.MagicMock()
# `_verify_python_installation` runs `exec_run(['which', 'python3'])` and
# checks `exit_code == 0`.
container.exec_run.return_value = mock.MagicMock(exit_code=0)
client.containers.run.return_value = container
return client
@mock.patch('google.adk.code_executors.container_code_executor.docker')
def test_container_is_hardened_by_default(mock_docker):
"""Networking is disabled and privileges are dropped by default."""
client = _mock_docker_client()
mock_docker.from_env.return_value = client
ContainerCodeExecutor(image='test-image')
_, kwargs = client.containers.run.call_args
# Untrusted model-generated code must not be able to reach the network
# (e.g. the cloud metadata endpoint) or escalate privileges by default.
assert kwargs['network_disabled']
assert kwargs['cap_drop'] == ['ALL']
assert kwargs['security_opt'] == ['no-new-privileges']
@mock.patch('google.adk.code_executors.container_code_executor.docker')
def test_container_network_can_be_explicitly_enabled(mock_docker):
"""Networking is left enabled when the caller opts in."""
client = _mock_docker_client()
mock_docker.from_env.return_value = client
ContainerCodeExecutor(image='test-image', network_enabled=True)
_, kwargs = client.containers.run.call_args
assert not kwargs['network_disabled']
@@ -0,0 +1,449 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from unittest.mock import MagicMock
from unittest.mock import patch
from google.adk.agents.invocation_context import InvocationContext
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
from google.adk.code_executors.gke_code_executor import GkeCodeExecutor
from kubernetes import client
from kubernetes import config
from kubernetes.client.rest import ApiException
import pytest
@pytest.fixture
def mock_invocation_context() -> InvocationContext:
"""Fixture for a mock InvocationContext."""
mock = MagicMock(spec=InvocationContext)
mock.invocation_id = "test-invocation-123"
return mock
@pytest.fixture(autouse=True)
def mock_k8s_config():
"""Fixture for auto-mocking Kubernetes config loading."""
with patch(
"google.adk.code_executors.gke_code_executor.config"
) as mock_config:
# Simulate fallback from in-cluster to kubeconfig
mock_config.ConfigException = config.ConfigException
mock_config.load_incluster_config.side_effect = config.ConfigException
yield mock_config
@pytest.fixture
def mock_k8s_clients():
"""Fixture for mock Kubernetes API clients."""
with patch(
"google.adk.code_executors.gke_code_executor.client"
) as mock_client_class:
mock_batch_v1 = MagicMock(spec=client.BatchV1Api)
mock_core_v1 = MagicMock(spec=client.CoreV1Api)
mock_client_class.BatchV1Api.return_value = mock_batch_v1
mock_client_class.CoreV1Api.return_value = mock_core_v1
yield {
"batch_v1": mock_batch_v1,
"core_v1": mock_core_v1,
}
class TestGkeCodeExecutor:
"""Unit tests for the GkeCodeExecutor."""
def test_init_defaults(self):
"""Tests that the executor initializes with correct default values."""
executor = GkeCodeExecutor()
assert executor.namespace == "default"
assert executor.image == "python:3.11-slim"
assert executor.timeout_seconds == 300
assert executor.cpu_requested == "200m"
assert executor.mem_limit == "512Mi"
assert executor.executor_type == "job"
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_init_with_overrides(self, mock_sandbox_client):
"""Tests that class attributes can be overridden at instantiation."""
executor = GkeCodeExecutor(
namespace="test-ns",
image="custom-python:latest",
timeout_seconds=60,
cpu_limit="1000m",
executor_type="sandbox",
)
assert executor.namespace == "test-ns"
assert executor.image == "custom-python:latest"
assert executor.timeout_seconds == 60
assert executor.cpu_limit == "1000m"
assert executor.executor_type == "sandbox"
assert executor.sandbox_template == "python-sandbox-template"
def test_init_backward_compatibility(self):
"""Tests that the executor can be initialized with positional arguments."""
executor = GkeCodeExecutor(
"/path/to/kubeconfig",
"test-context",
namespace="test-ns",
image="test-image",
timeout_seconds=100,
executor_type="job",
cpu_requested="100m",
mem_requested="128Mi",
cpu_limit="200m",
mem_limit="256Mi",
)
assert executor.namespace == "test-ns"
assert executor.image == "test-image"
assert executor.timeout_seconds == 100
assert executor.executor_type == "job"
assert executor.cpu_requested == "100m"
assert executor.mem_requested == "128Mi"
assert executor.cpu_limit == "200m"
assert executor.mem_limit == "256Mi"
assert executor.kubeconfig_path == "/path/to/kubeconfig"
assert executor.kubeconfig_context == "test-context"
def test_init_partial_positional_args(self):
"""Tests initialization with partial positional arguments."""
executor = GkeCodeExecutor("/path/to/kubeconfig")
assert executor.kubeconfig_path == "/path/to/kubeconfig"
assert executor.kubeconfig_context is None
def test_init_mixed_args(self):
"""Tests initialization with mixed positional and keyword arguments."""
executor = GkeCodeExecutor(
"/path/to/kubeconfig",
kubeconfig_context="test-context",
namespace="test-ns",
)
assert executor.kubeconfig_path == "/path/to/kubeconfig"
def test_init_sandbox_missing_dependency(self):
"""Tests that init raises ImportError if k8s-agent-sandbox is missing."""
with patch(
"google.adk.code_executors.gke_code_executor.SandboxClient", None
):
with pytest.raises(ImportError, match="k8s-agent-sandbox not found"):
GkeCodeExecutor(executor_type="sandbox")
GkeCodeExecutor(executor_type="sandbox")
@patch("google.adk.code_executors.gke_code_executor.Watch")
def test_execute_code_success(
self,
mock_watch,
mock_k8s_clients,
mock_invocation_context,
):
"""Tests the happy path for successful code execution."""
# Setup Mocks
mock_job = MagicMock()
mock_job.status.succeeded = True
mock_job.status.failed = None
mock_watch.return_value.stream.return_value = [{"object": mock_job}]
mock_pod_list = MagicMock()
mock_pod_list.items = [MagicMock()]
mock_pod_list.items[0].metadata.name = "test-pod-name"
mock_k8s_clients["core_v1"].list_namespaced_pod.return_value = mock_pod_list
mock_k8s_clients["core_v1"].read_namespaced_pod_log.return_value = (
"hello world"
)
# Execute
executor = GkeCodeExecutor()
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
# Assert
assert result.stdout == "hello world"
assert result.stderr == ""
mock_k8s_clients[
"core_v1"
].create_namespaced_config_map.assert_called_once()
mock_k8s_clients["batch_v1"].create_namespaced_job.assert_called_once()
mock_k8s_clients["core_v1"].patch_namespaced_config_map.assert_called_once()
mock_k8s_clients["core_v1"].read_namespaced_pod_log.assert_called_once()
@patch("google.adk.code_executors.gke_code_executor.Watch")
def test_execute_code_job_failed(
self,
mock_watch,
mock_k8s_clients,
mock_invocation_context,
):
"""Tests the path where the Kubernetes Job fails."""
mock_job = MagicMock()
mock_job.status.succeeded = None
mock_job.status.failed = True
mock_watch.return_value.stream.return_value = [{"object": mock_job}]
mock_k8s_clients["core_v1"].read_namespaced_pod_log.return_value = (
"Traceback...\nValueError: failure"
)
executor = GkeCodeExecutor()
result = executor.execute_code(
mock_invocation_context, CodeExecutionInput(code="fail")
)
assert result.stdout == ""
assert "Job failed. Logs:" in result.stderr
assert "ValueError: failure" in result.stderr
def test_execute_code_api_exception(
self, mock_k8s_clients, mock_invocation_context
):
"""Tests handling of an ApiException from the K8s client."""
mock_k8s_clients["core_v1"].create_namespaced_config_map.side_effect = (
ApiException(reason="Test API Error")
)
executor = GkeCodeExecutor()
result = executor.execute_code(
mock_invocation_context, CodeExecutionInput(code="...")
)
assert result.stdout == ""
assert "Kubernetes API error: Test API Error" in result.stderr
@patch("google.adk.code_executors.gke_code_executor.Watch")
def test_execute_code_timeout(
self,
mock_watch,
mock_k8s_clients,
mock_invocation_context,
):
"""Tests the case where the job watch times out."""
mock_watch.return_value.stream.return_value = (
[]
) # Empty stream simulates timeout
mock_k8s_clients["core_v1"].read_namespaced_pod_log.return_value = (
"Still running..."
)
executor = GkeCodeExecutor(timeout_seconds=1)
result = executor.execute_code(
mock_invocation_context, CodeExecutionInput(code="...")
)
assert result.stdout == ""
assert "Executor timed out" in result.stderr
assert "did not complete within 1s" in result.stderr
assert "Pod Logs:\nStill running..." in result.stderr
def test_create_job_manifest_structure(self, mock_invocation_context):
"""Tests the correctness of the generated Job manifest."""
executor = GkeCodeExecutor(namespace="test-ns", image="test-img:v1")
job = executor._create_job_manifest(
"test-job", "test-cm", mock_invocation_context
)
# Check top-level properties
assert isinstance(job, client.V1Job)
assert job.api_version == "batch/v1"
assert job.kind == "Job"
assert job.metadata.name == "test-job"
assert job.spec.backoff_limit == 0
assert job.spec.ttl_seconds_after_finished == 600
# Check pod template properties
pod_spec = job.spec.template.spec
assert pod_spec.restart_policy == "Never"
assert pod_spec.runtime_class_name == "gvisor"
assert len(pod_spec.tolerations) == 1
assert pod_spec.tolerations[0].value == "gvisor"
assert len(pod_spec.volumes) == 1
assert pod_spec.volumes[0].name == "code-volume"
assert pod_spec.volumes[0].config_map.name == "test-cm"
# Check container properties
container = pod_spec.containers[0]
assert container.name == "code-runner"
assert container.image == "test-img:v1"
assert container.command == ["python3", "/app/code.py"]
# Check security context
sec_context = container.security_context
assert sec_context.run_as_non_root is True
assert sec_context.run_as_user == 1001
assert sec_context.allow_privilege_escalation is False
assert sec_context.read_only_root_filesystem is True
assert sec_context.capabilities.drop == ["ALL"]
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_execute_code_forks_to_sandbox(
self,
mock_sandbox_client,
mock_invocation_context,
mock_k8s_clients,
):
"""Tests execute_code with executor_type='sandbox'.
Verifies that execute_code uses SandboxClient when executor_type is set to
'sandbox'.
"""
# Setup Sandbox mock
mock_sandbox_instance = (
mock_sandbox_client.return_value.__enter__.return_value
)
mock_run_result = MagicMock()
mock_run_result.stdout = "sandbox stdout"
mock_run_result.stderr = None
mock_sandbox_instance.run.return_value = mock_run_result
# Instantiate with sandbox type
executor = GkeCodeExecutor(executor_type="sandbox")
code_input = CodeExecutionInput(code='print("sandbox")')
# Execute
result = executor.execute_code(mock_invocation_context, code_input)
# Assertions
assert result.stdout == "sandbox stdout"
# Verify SandboxClient was used
mock_sandbox_client.assert_called_once()
mock_sandbox_instance.run.assert_called_once()
# Verify Job path was NOT taken
mock_k8s_clients["batch_v1"].create_namespaced_job.assert_not_called()
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_execute_code_sandbox_connection_error(
self,
mock_sandbox_client,
mock_invocation_context,
):
"""Tests handling of exceptions from SandboxClient."""
# Setup Sandbox mock to raise exception
mock_sandbox_client.return_value.__enter__.side_effect = Exception(
"Connection failed"
)
# Instantiate with sandbox type
executor = GkeCodeExecutor(executor_type="sandbox")
code_input = CodeExecutionInput(code='print("sandbox")')
# Execute & Assert
with pytest.raises(Exception, match="Connection failed"):
executor.execute_code(mock_invocation_context, code_input)
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_execute_code_sandbox_runtime_error(
self,
mock_sandbox_client,
mock_invocation_context,
):
"""Tests handling of RuntimeError from SandboxClient."""
mock_sandbox_client.return_value.__enter__.side_effect = RuntimeError(
"Gateway not found"
)
executor = GkeCodeExecutor(executor_type="sandbox")
code_input = CodeExecutionInput(code='print("sandbox")')
with pytest.raises(
RuntimeError, match="Sandbox infrastructure error: Gateway not found"
):
executor.execute_code(mock_invocation_context, code_input)
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_execute_code_sandbox_timeout_error(
self,
mock_sandbox_client,
mock_invocation_context,
):
"""Tests handling of TimeoutError from SandboxClient."""
mock_sandbox_client.return_value.__enter__.side_effect = TimeoutError(
"Execution timed out"
)
executor = GkeCodeExecutor(executor_type="sandbox")
code_input = CodeExecutionInput(code='print("sandbox")')
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert "Sandbox timed out: Execution timed out" in result.stderr
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
@patch("google.adk.code_executors.gke_code_executor.Watch")
def test_execute_code_forks_to_job(
self,
mock_watch,
mock_sandbox_client,
mock_invocation_context,
mock_k8s_clients,
):
"""Tests that execute_code uses K8s Job when executor_type='job'."""
# Setup K8s Job mocks (success path)
mock_job = MagicMock()
mock_job.status.succeeded = True
mock_watch.return_value.stream.return_value = [{"object": mock_job}]
mock_pod = MagicMock()
mock_pod.metadata.name = "pod-1"
mock_k8s_clients["core_v1"].list_namespaced_pod.return_value.items = [
mock_pod
]
mock_k8s_clients["core_v1"].read_namespaced_pod_log.return_value = (
"job stdout"
)
# Instantiate with job type
executor = GkeCodeExecutor(executor_type="job")
code_input = CodeExecutionInput(code='print("job")')
# Execute
result = executor.execute_code(mock_invocation_context, code_input)
# Assertions
assert result.stdout == "job stdout"
# Verify Job path WAS taken
mock_k8s_clients["batch_v1"].create_namespaced_job.assert_called_once()
# Verify SandboxClient was NOT used
mock_sandbox_client.assert_not_called()
@patch("google.adk.code_executors.gke_code_executor.SandboxClient")
def test_execute_in_sandbox_returns_stderr(
self,
mock_sandbox_client,
mock_invocation_context,
):
"""Tests that stderr from the sandbox run is propagated to the result."""
# Setup Sandbox mock
mock_sandbox_instance = (
mock_sandbox_client.return_value.__enter__.return_value
)
mock_run_result = MagicMock()
mock_run_result.stdout = ""
mock_run_result.stderr = "oops\n"
mock_sandbox_instance.run.return_value = mock_run_result
# Instantiate with sandbox type
executor = GkeCodeExecutor(executor_type="sandbox")
code_input = CodeExecutionInput(
code="import sys; print('oops', file=sys.stderr)"
)
# Execute
result = executor.execute_code(mock_invocation_context, code_input)
# Assertions
assert result.stdout == ""
assert result.stderr == "oops\n"
mock_sandbox_instance.write.assert_called_with("script.py", code_input.code)
mock_sandbox_instance.run.assert_called_with("python3 script.py")
@@ -0,0 +1,133 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import textwrap
from unittest.mock import MagicMock
from google.adk.agents.base_agent import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.code_executors.code_execution_utils import CodeExecutionInput
from google.adk.code_executors.code_execution_utils import CodeExecutionResult
from google.adk.code_executors.unsafe_local_code_executor import UnsafeLocalCodeExecutor
from google.adk.sessions.base_session_service import BaseSessionService
from google.adk.sessions.session import Session
import pytest
@pytest.fixture
def mock_invocation_context() -> InvocationContext:
"""Provides a mock InvocationContext."""
mock_agent = MagicMock(spec=BaseAgent)
mock_session = MagicMock(spec=Session)
mock_session_service = MagicMock(spec=BaseSessionService)
return InvocationContext(
invocation_id="test_invocation",
agent=mock_agent,
session=mock_session,
session_service=mock_session_service,
)
class TestUnsafeLocalCodeExecutor:
def test_init_default(self):
executor = UnsafeLocalCodeExecutor()
assert not executor.stateful
assert not executor.optimize_data_file
def test_init_stateful_raises_error(self):
with pytest.raises(
ValueError,
match="Cannot set `stateful=True` in UnsafeLocalCodeExecutor.",
):
UnsafeLocalCodeExecutor(stateful=True)
def test_init_optimize_data_file_raises_error(self):
with pytest.raises(
ValueError,
match=(
"Cannot set `optimize_data_file=True` in UnsafeLocalCodeExecutor."
),
):
UnsafeLocalCodeExecutor(optimize_data_file=True)
def test_execute_code_simple_print(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code='print("hello world")')
result = executor.execute_code(mock_invocation_context, code_input)
assert isinstance(result, CodeExecutionResult)
assert result.stdout == "hello world\n"
assert result.stderr == ""
assert result.output_files == []
def test_execute_code_with_error(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code='raise ValueError("Test error")')
result = executor.execute_code(mock_invocation_context, code_input)
assert isinstance(result, CodeExecutionResult)
assert result.stdout == ""
assert "Test error" in result.stderr
assert result.output_files == []
def test_execute_code_variable_assignment(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code="x = 10\nprint(x * 2)")
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == "20\n"
assert result.stderr == ""
def test_execute_code_empty(self, mock_invocation_context: InvocationContext):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code="")
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert result.stderr == ""
def test_execute_code_nested_function_call(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor()
code_input = CodeExecutionInput(code=(textwrap.dedent("""
def helper(name):
return f'hi {name}'
def run():
print(helper('ada'))
run()
""")))
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stderr == ""
assert result.stdout == "hi ada\n"
def test_execute_code_timeout(
self, mock_invocation_context: InvocationContext
):
executor = UnsafeLocalCodeExecutor(timeout_seconds=1)
code_input = CodeExecutionInput(code="import time\ntime.sleep(2)")
result = executor.execute_code(mock_invocation_context, code_input)
assert result.stdout == ""
assert "Code execution timed out after 1 seconds." in result.stderr