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

365 lines
12 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.
"""Unit tests for the SandboxClient class."""
import base64
import json
import unittest
from unittest.mock import MagicMock
from unittest.mock import patch
from google.adk.integrations.vmaas.sandbox_client import SandboxClient
def _make_response(data: dict) -> MagicMock:
"""Create a mock HttpResponse with a JSON body."""
response = MagicMock()
response.body = json.dumps(data)
return response
class TestSandboxClient(unittest.IsolatedAsyncioTestCase):
"""Tests for SandboxClient."""
def setUp(self):
"""Set up test fixtures."""
self.mock_vertexai_client = MagicMock()
self.mock_sandbox = MagicMock()
self.access_token = "test_token_12345"
self.client = SandboxClient(
vertexai_client=self.mock_vertexai_client,
sandbox=self.mock_sandbox,
access_token=self.access_token,
)
def test_init(self):
"""Test client initialization."""
self.assertEqual(self.client._client, self.mock_vertexai_client)
self.assertEqual(self.client._sandbox, self.mock_sandbox)
self.assertEqual(self.client._access_token, self.access_token)
def test_update_access_token(self):
"""Test updating access token."""
new_token = "new_token_67890"
self.client.update_access_token(new_token)
self.assertEqual(self.client._access_token, new_token)
@patch("asyncio.to_thread")
async def test_make_cdp_request(self, mock_to_thread):
"""Test making a single CDP request."""
mock_to_thread.return_value = _make_response({"result": "success"})
result = await self.client.make_cdp_request(
"Page.navigate", {"url": "https://example.com"}
)
self.assertEqual(result, {"result": "success"})
mock_to_thread.assert_called_once()
call_args = mock_to_thread.call_args
# First positional arg is the send_command method
self.assertEqual(
call_args[0][0],
self.mock_vertexai_client.agent_engines.sandboxes.send_command,
)
# Check keyword args
self.assertEqual(call_args[1]["http_method"], "POST")
self.assertEqual(call_args[1]["path"], "cdp")
self.assertEqual(call_args[1]["access_token"], self.access_token)
self.assertEqual(call_args[1]["sandbox_environment"], self.mock_sandbox)
self.assertEqual(
call_args[1]["request_dict"],
{"command": "Page.navigate", "params": {"url": "https://example.com"}},
)
@patch("asyncio.to_thread")
async def test_make_cdp_batch_request_with_batch_endpoint(
self, mock_to_thread
):
"""Test making a batch CDP request using batch endpoint."""
mock_to_thread.return_value = _make_response(
{"results": [{"status": "success"}, {"status": "success"}]}
)
commands = [
{
"command": "Input.dispatchMouseEvent",
"params": {"type": "mousePressed"},
},
{
"command": "Input.dispatchMouseEvent",
"params": {"type": "mouseReleased"},
},
]
result = await self.client.make_cdp_batch_request(commands)
# Should have 2 results from batch
self.assertEqual(len(result), 2)
# Should have made 1 call to /cdps
self.assertEqual(mock_to_thread.call_count, 1)
call_args = mock_to_thread.call_args
self.assertEqual(call_args[1]["path"], "cdps")
@patch("asyncio.to_thread")
async def test_make_cdp_batch_request_fallback_sequential(
self, mock_to_thread
):
"""Test batch CDP falls back to sequential when batch endpoint fails."""
# First call (to /cdps) fails with 404
# Subsequent calls (to /cdp) succeed
def side_effect(*args, **kwargs):
if kwargs.get("path") == "cdps":
raise Exception("404 Not Found")
return _make_response({"result": "ok"})
mock_to_thread.side_effect = side_effect
commands = [
{"command": "Command1", "params": {}},
{"command": "Command2", "params": {}},
]
result = await self.client.make_cdp_batch_request(commands)
# Should have 2 results (sequential fallback)
self.assertEqual(len(result), 2)
self.assertEqual(result[0]["status"], "success")
self.assertEqual(result[1]["status"], "success")
# Should have made 3 calls (1 failed /cdps + 2 sequential /cdp)
self.assertEqual(mock_to_thread.call_count, 3)
@patch("asyncio.to_thread")
async def test_get_screenshot(self, mock_to_thread):
"""Test capturing a screenshot."""
# Create a simple PNG-like base64 data
png_data = b"\x89PNG\r\n\x1a\n"
base64_data = base64.b64encode(png_data).decode()
mock_to_thread.return_value = _make_response({"data": base64_data})
result = await self.client.get_screenshot()
self.assertEqual(result, png_data)
call_args = mock_to_thread.call_args
self.assertEqual(
call_args[1]["request_dict"]["command"], "Page.captureScreenshot"
)
@patch("asyncio.to_thread")
async def test_get_current_url(self, mock_to_thread):
"""Test getting the current URL."""
mock_to_thread.return_value = _make_response({
"active_tab_id": "tab1",
"all_tabs": [
{"id": "tab1", "url": "https://example.com", "title": "Example"},
],
})
result = await self.client.get_current_url()
self.assertEqual(result, "https://example.com")
call_args = mock_to_thread.call_args
self.assertEqual(call_args[1]["path"], "tabs")
self.assertEqual(call_args[1]["http_method"], "GET")
@patch("asyncio.to_thread")
async def test_get_current_url_no_active_tab(self, mock_to_thread):
"""Test getting URL when no tab is active."""
mock_to_thread.return_value = _make_response({
"active_tab_id": None,
"all_tabs": [],
})
result = await self.client.get_current_url()
self.assertIsNone(result)
@patch("asyncio.to_thread")
async def test_navigate(self, mock_to_thread):
"""Test navigating to a URL."""
mock_to_thread.return_value = _make_response({"frameId": "frame123"})
result = await self.client.navigate("https://example.com")
self.assertEqual(result, {"frameId": "frame123"})
call_args = mock_to_thread.call_args
self.assertEqual(
call_args[1]["request_dict"],
{"command": "Page.navigate", "params": {"url": "https://example.com"}},
)
@patch("asyncio.to_thread")
async def test_click_at(self, mock_to_thread):
"""Test clicking at coordinates."""
# Return success for batch endpoint
mock_to_thread.return_value = _make_response({"results": [{}, {}]})
await self.client.click_at(100, 200)
# Should call batch endpoint
call_args = mock_to_thread.call_args
self.assertEqual(call_args[1]["path"], "cdps")
commands = call_args[1]["request_dict"]["commands"]
self.assertEqual(len(commands), 2)
self.assertEqual(commands[0]["params"]["type"], "mousePressed")
self.assertEqual(commands[1]["params"]["type"], "mouseReleased")
@patch("asyncio.to_thread")
async def test_hover_at(self, mock_to_thread):
"""Test hovering at coordinates."""
mock_to_thread.return_value = _make_response({})
await self.client.hover_at(150, 250)
call_args = mock_to_thread.call_args
self.assertEqual(
call_args[1]["request_dict"]["params"],
{"type": "mouseMoved", "x": 150, "y": 250},
)
@patch("asyncio.to_thread")
async def test_scroll_at_down(self, mock_to_thread):
"""Test scrolling down."""
mock_to_thread.return_value = _make_response({})
await self.client.scroll_at(100, 200, "down", 300)
call_args = mock_to_thread.call_args
params = call_args[1]["request_dict"]["params"]
self.assertEqual(params["type"], "mouseWheel")
self.assertEqual(params["x"], 100)
self.assertEqual(params["y"], 200)
self.assertEqual(params["deltaX"], 0)
self.assertEqual(params["deltaY"], 300) # Positive for down
@patch("asyncio.to_thread")
async def test_scroll_at_up(self, mock_to_thread):
"""Test scrolling up."""
mock_to_thread.return_value = _make_response({})
await self.client.scroll_at(100, 200, "up", 300)
call_args = mock_to_thread.call_args
params = call_args[1]["request_dict"]["params"]
self.assertEqual(params["deltaY"], -300) # Negative for up
@patch("asyncio.to_thread")
async def test_go_back(self, mock_to_thread):
"""Test navigating back."""
# First call returns navigation history, second navigates
mock_to_thread.side_effect = [
_make_response({
"currentIndex": 1,
"entries": [
{"id": 1, "url": "https://first.com"},
{"id": 2, "url": "https://second.com"},
],
}),
_make_response({}), # Navigation response
]
result = await self.client.go_back()
self.assertTrue(result)
self.assertEqual(mock_to_thread.call_count, 2)
@patch("asyncio.to_thread")
async def test_go_back_at_beginning(self, mock_to_thread):
"""Test navigating back when at beginning of history."""
mock_to_thread.return_value = _make_response({
"currentIndex": 0,
"entries": [{"id": 1, "url": "https://first.com"}],
})
result = await self.client.go_back()
self.assertFalse(result)
# Should only call once (to get history)
self.assertEqual(mock_to_thread.call_count, 1)
@patch("asyncio.to_thread")
async def test_type_text_with_clear_and_enter(self, mock_to_thread):
"""Test typing text with clear and enter options."""
# Return success for batch endpoint
mock_to_thread.return_value = _make_response({"results": [{}] * 7})
await self.client.type_text(
"hello", press_enter=True, clear_before_typing=True
)
# Should have: Ctrl+A down, Ctrl+A up, Delete down, Delete up,
# insertText, Enter down, Enter up = 7 commands in batch
call_args = mock_to_thread.call_args
commands = call_args[1]["request_dict"]["commands"]
self.assertEqual(len(commands), 7)
@patch("asyncio.to_thread")
async def test_key_combination(self, mock_to_thread):
"""Test pressing key combinations."""
mock_to_thread.return_value = _make_response({"results": [{}] * 4})
await self.client.key_combination(["control", "c"])
# Should have: Control down, c down, c up, Control up = 4 commands
call_args = mock_to_thread.call_args
commands = call_args[1]["request_dict"]["commands"]
self.assertEqual(len(commands), 4)
@patch("asyncio.to_thread")
async def test_drag_and_drop(self, mock_to_thread):
"""Test drag and drop operation."""
mock_to_thread.return_value = _make_response({"results": [{}] * 4})
await self.client.drag_and_drop(10, 20, 100, 200)
# Should have: mouseMoved (start), mousePressed, mouseMoved (end),
# mouseReleased = 4 commands
call_args = mock_to_thread.call_args
commands = call_args[1]["request_dict"]["commands"]
self.assertEqual(len(commands), 4)
@patch("asyncio.to_thread")
async def test_health_check_healthy(self, mock_to_thread):
"""Test health check when sandbox is healthy."""
mock_to_thread.return_value = _make_response({"status": "healthy"})
result = await self.client.health_check()
self.assertTrue(result)
call_args = mock_to_thread.call_args
self.assertEqual(call_args[1]["http_method"], "GET")
self.assertEqual(call_args[1]["path"], "")
@patch("asyncio.to_thread")
async def test_health_check_unhealthy(self, mock_to_thread):
"""Test health check when sandbox is unhealthy."""
mock_to_thread.return_value = _make_response({"status": "unhealthy"})
result = await self.client.health_check()
self.assertFalse(result)
@patch("asyncio.to_thread")
async def test_health_check_exception(self, mock_to_thread):
"""Test health check when request fails."""
mock_to_thread.side_effect = Exception("Connection failed")
result = await self.client.health_check()
self.assertFalse(result)
if __name__ == "__main__":
unittest.main()