Files
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

243 lines
7.1 KiB
Python

# 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 DaytonaEnvironment."""
from unittest import mock
import daytona
from daytona import CreateSandboxFromImageParams
from daytona import CreateSandboxFromSnapshotParams
from daytona import DaytonaError
from daytona import DaytonaNotFoundError
from google.adk.integrations.daytona._daytona_environment import DaytonaEnvironment
import pytest
def _make_sandbox() -> mock.MagicMock:
"""Build a mock AsyncSandbox with async method stubs."""
sandbox = mock.MagicMock(name="AsyncSandbox")
sandbox.delete = mock.AsyncMock()
sandbox.process.exec = mock.AsyncMock()
sandbox.fs.download_file = mock.AsyncMock()
sandbox.fs.upload_file = mock.AsyncMock()
sandbox.fs.create_folder = mock.AsyncMock()
sandbox.refresh_activity = mock.AsyncMock()
return sandbox
@pytest.fixture(name="sandbox")
def _sandbox() -> mock.MagicMock:
return _make_sandbox()
@pytest.fixture(name="daytona_patch")
def _daytona_patch(sandbox: mock.MagicMock):
"""Patch AsyncDaytona to return a mock client."""
mock_client = mock.MagicMock(name="AsyncDaytona")
mock_client.create = mock.AsyncMock(return_value=sandbox)
with mock.patch.object(daytona, "AsyncDaytona", autospec=True) as mock_class:
mock_class.return_value = mock_client
yield mock_class
async def test_initialize_creates_sandbox(daytona_patch, sandbox):
env = DaytonaEnvironment(image="custom-image", env_vars={"A": "1"})
assert env.is_initialized is False
await env.initialize()
assert env.is_initialized is True
daytona_patch.assert_called_once()
client = daytona_patch.return_value
client.create.assert_awaited_once()
args, _ = client.create.call_args
params = args[0]
assert isinstance(params, CreateSandboxFromImageParams)
assert params.image == "custom-image"
assert params.env_vars == {"A": "1"}
assert params.auto_stop_interval == 5
assert params.auto_delete_interval == 0
assert env._sandbox is sandbox
async def test_initialize_creates_sandbox_default(daytona_patch, sandbox):
env = DaytonaEnvironment(env_vars={"B": "2"})
await env.initialize()
daytona_patch.assert_called_once()
client = daytona_patch.return_value
client.create.assert_awaited_once()
args, _ = client.create.call_args
params = args[0]
assert isinstance(params, CreateSandboxFromSnapshotParams)
assert params.language == "python"
assert params.env_vars == {"B": "2"}
assert params.auto_stop_interval == 5
assert params.auto_delete_interval == 0
assert env._sandbox is sandbox
async def test_initialize_is_idempotent(daytona_patch, sandbox):
env = DaytonaEnvironment()
await env.initialize()
await env.initialize()
client = daytona_patch.return_value
client.create.assert_awaited_once()
async def test_close_deletes_sandbox_and_is_idempotent(daytona_patch, sandbox):
env = DaytonaEnvironment()
await env.initialize()
assert env.is_initialized is True
await env.close()
sandbox.delete.assert_awaited_once()
assert env._sandbox is None
assert env.is_initialized is False
# Second close is a no-op.
await env.close()
sandbox.delete.assert_awaited_once()
async def test_working_dir_requires_initialize():
env = DaytonaEnvironment()
with pytest.raises(RuntimeError):
_ = env.working_dir
async def test_execute_before_initialize_raises():
env = DaytonaEnvironment()
with pytest.raises(RuntimeError):
await env.execute("echo hi")
async def test_execute_success(daytona_patch, sandbox):
class MockArtifacts:
stdout = "out"
class MockResponse:
exit_code = 0
artifacts = MockArtifacts()
sandbox.process.exec.return_value = MockResponse()
env = DaytonaEnvironment()
await env.initialize()
result = await env.execute("echo out")
assert result.exit_code == 0
assert result.stdout == "out"
assert result.stderr == ""
assert result.timed_out is False
sandbox.refresh_activity.assert_awaited_once()
async def test_execute_timeout(daytona_patch, sandbox):
# Simulate a Daytona timeout error
sandbox.process.exec.side_effect = DaytonaError("timeout occurred")
env = DaytonaEnvironment()
await env.initialize()
result = await env.execute("sleep 999")
assert result.timed_out is True
async def test_read_file_returns_bytes(daytona_patch, sandbox):
sandbox.fs.download_file.return_value = b"data"
env = DaytonaEnvironment()
await env.initialize()
data = await env.read_file("notes.txt")
assert data == b"data"
sandbox.fs.download_file.assert_awaited_once_with("/workspaces/notes.txt")
sandbox.refresh_activity.assert_awaited_once()
async def test_read_file_absolute_path_passthrough(daytona_patch, sandbox):
sandbox.fs.download_file.return_value = b"x"
env = DaytonaEnvironment()
await env.initialize()
await env.read_file("/etc/hostname")
sandbox.fs.download_file.assert_awaited_once_with("/etc/hostname")
async def test_read_file_missing_raises(daytona_patch, sandbox):
sandbox.fs.download_file.return_value = None
env = DaytonaEnvironment()
await env.initialize()
with pytest.raises(FileNotFoundError):
await env.read_file("missing.txt")
async def test_write_file_resolves_relative_path(daytona_patch, sandbox):
env = DaytonaEnvironment()
await env.initialize()
await env.write_file("sub/out.txt", "hello")
sandbox.refresh_activity.assert_awaited_once()
sandbox.fs.upload_file.assert_awaited_once_with(
b"hello", "/workspaces/sub/out.txt"
)
async def test_initialize_propagates_api_key_and_url(daytona_patch, sandbox):
from daytona import DaytonaConfig
env = DaytonaEnvironment(api_key="my-key", api_url="my-url")
await env.initialize()
daytona_patch.assert_called_once()
_, kwargs = daytona_patch.call_args
config = kwargs.get("config")
assert isinstance(config, DaytonaConfig)
assert config.api_key == "my-key"
assert config.api_url == "my-url"
async def test_write_file_creates_parent_directory(daytona_patch, sandbox):
env = DaytonaEnvironment()
await env.initialize()
await env.write_file("sub/nested/file.txt", "content")
sandbox.fs.create_folder.assert_has_calls([
mock.call("/workspaces", mode="755"),
mock.call("/workspaces/sub", mode="755"),
mock.call("/workspaces/sub/nested", mode="755"),
])
sandbox.fs.upload_file.assert_awaited_once_with(
b"content", "/workspaces/sub/nested/file.txt"
)
async def test_read_file_raises_file_not_found_on_daytona_not_found(
daytona_patch, sandbox
):
sandbox.fs.download_file.side_effect = DaytonaNotFoundError("not found")
env = DaytonaEnvironment()
await env.initialize()
with pytest.raises(FileNotFoundError):
await env.read_file("missing.txt")