chore: import upstream snapshot with attribution
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

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,341 @@
# 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 base_computer module."""
from typing import Literal
from google.adk.tools.computer_use.base_computer import BaseComputer
from google.adk.tools.computer_use.base_computer import ComputerEnvironment
from google.adk.tools.computer_use.base_computer import ComputerState
import pytest
class TestComputerEnvironment:
"""Test cases for ComputerEnvironment enum."""
def test_valid_environments(self):
"""Test valid environment values."""
assert (
ComputerEnvironment.ENVIRONMENT_UNSPECIFIED == "ENVIRONMENT_UNSPECIFIED"
)
assert ComputerEnvironment.ENVIRONMENT_BROWSER == "ENVIRONMENT_BROWSER"
def test_invalid_environment_raises(self):
"""Test that invalid environment values raise ValueError."""
with pytest.raises(ValueError):
ComputerEnvironment("INVALID_ENVIRONMENT")
def test_string_representation(self):
"""Test string representation of enum values."""
assert (
ComputerEnvironment.ENVIRONMENT_BROWSER.value == "ENVIRONMENT_BROWSER"
)
assert (
ComputerEnvironment.ENVIRONMENT_UNSPECIFIED.value
== "ENVIRONMENT_UNSPECIFIED"
)
class TestComputerState:
"""Test cases for ComputerState Pydantic model."""
def test_default_initialization(self):
"""Test ComputerState with default values."""
state = ComputerState()
assert state.screenshot is None
assert state.url is None
def test_initialization_with_screenshot(self):
"""Test ComputerState with screenshot data."""
screenshot_data = b"fake_png_data"
state = ComputerState(screenshot=screenshot_data)
assert state.screenshot == screenshot_data
assert state.url is None
def test_initialization_with_url(self):
"""Test ComputerState with URL."""
url = "https://example.com"
state = ComputerState(url=url)
assert state.screenshot is None
assert state.url == url
def test_initialization_with_all_fields(self):
"""Test ComputerState with all fields provided."""
screenshot_data = b"fake_png_data"
url = "https://example.com"
state = ComputerState(screenshot=screenshot_data, url=url)
assert state.screenshot == screenshot_data
assert state.url == url
def test_field_validation(self):
"""Test field validation for ComputerState."""
# Test that bytes are accepted for screenshot
state = ComputerState(screenshot=b"test_data")
assert state.screenshot == b"test_data"
# Test that string is accepted for URL
state = ComputerState(url="https://test.com")
assert state.url == "https://test.com"
def test_model_serialization(self):
"""Test that ComputerState can be serialized."""
state = ComputerState(screenshot=b"test", url="https://example.com")
# Should not raise an exception
model_dict = state.model_dump()
assert "screenshot" in model_dict
assert "url" in model_dict
class MockComputer(BaseComputer):
"""Mock implementation of BaseComputer for testing."""
def __init__(self):
self.initialized = False
self.closed = False
async def screen_size(self) -> tuple[int, int]:
return (1920, 1080)
async def open_web_browser(self) -> ComputerState:
return ComputerState(url="https://example.com")
async def click_at(self, x: int, y: int) -> ComputerState:
return ComputerState(url="https://example.com")
async def hover_at(self, x: int, y: int) -> ComputerState:
return ComputerState(url="https://example.com")
async def type_text_at(
self,
x: int,
y: int,
text: str,
press_enter: bool = True,
clear_before_typing: bool = True,
) -> ComputerState:
return ComputerState(url="https://example.com")
async def scroll_document(
self, direction: Literal["up", "down", "left", "right"]
) -> ComputerState:
return ComputerState(url="https://example.com")
async def scroll_at(
self,
x: int,
y: int,
direction: Literal["up", "down", "left", "right"],
magnitude: int,
) -> ComputerState:
return ComputerState(url="https://example.com")
async def wait(self, seconds: int) -> ComputerState:
return ComputerState(url="https://example.com")
async def go_back(self) -> ComputerState:
return ComputerState(url="https://example.com")
async def go_forward(self) -> ComputerState:
return ComputerState(url="https://example.com")
async def search(self) -> ComputerState:
return ComputerState(url="https://search.example.com")
async def navigate(self, url: str) -> ComputerState:
return ComputerState(url=url)
async def key_combination(self, keys: list[str]) -> ComputerState:
return ComputerState(url="https://example.com")
async def drag_and_drop(
self, x: int, y: int, destination_x: int, destination_y: int
) -> ComputerState:
return ComputerState(url="https://example.com")
async def current_state(self) -> ComputerState:
return ComputerState(
url="https://example.com", screenshot=b"screenshot_data"
)
async def initialize(self) -> None:
self.initialized = True
async def close(self) -> None:
self.closed = True
async def environment(self) -> ComputerEnvironment:
return ComputerEnvironment.ENVIRONMENT_BROWSER
class TestBaseComputer:
"""Test cases for BaseComputer abstract base class."""
@pytest.fixture
def mock_computer(self) -> MockComputer:
"""Fixture providing a mock computer implementation."""
return MockComputer()
def test_cannot_instantiate_abstract_class(self):
"""Test that BaseComputer cannot be instantiated directly."""
import pytest
with pytest.raises(TypeError):
BaseComputer() # Should raise TypeError because it's abstract
@pytest.mark.asyncio
async def test_screen_size(self, mock_computer):
"""Test screen_size method."""
size = await mock_computer.screen_size()
assert size == (1920, 1080)
assert isinstance(size, tuple)
assert len(size) == 2
@pytest.mark.asyncio
async def test_open_web_browser(self, mock_computer):
"""Test open_web_browser method."""
state = await mock_computer.open_web_browser()
assert isinstance(state, ComputerState)
assert state.url == "https://example.com"
@pytest.mark.asyncio
async def test_click_at(self, mock_computer):
"""Test click_at method."""
state = await mock_computer.click_at(100, 200)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_hover_at(self, mock_computer):
"""Test hover_at method."""
state = await mock_computer.hover_at(150, 250)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_type_text_at(self, mock_computer):
"""Test type_text_at method with different parameters."""
# Test with default parameters
state = await mock_computer.type_text_at(100, 200, "Hello World")
assert isinstance(state, ComputerState)
# Test with custom parameters
state = await mock_computer.type_text_at(
100, 200, "Hello", press_enter=False, clear_before_typing=False
)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_scroll_document(self, mock_computer):
"""Test scroll_document method with different directions."""
directions = ["up", "down", "left", "right"]
for direction in directions:
state = await mock_computer.scroll_document(direction)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_scroll_at(self, mock_computer):
"""Test scroll_at method."""
state = await mock_computer.scroll_at(100, 200, "down", 5)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_wait(self, mock_computer):
"""Test wait method."""
state = await mock_computer.wait(5)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_go_back(self, mock_computer):
"""Test go_back method."""
state = await mock_computer.go_back()
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_go_forward(self, mock_computer):
"""Test go_forward method."""
state = await mock_computer.go_forward()
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_search(self, mock_computer):
"""Test search method."""
state = await mock_computer.search()
assert isinstance(state, ComputerState)
assert state.url == "https://search.example.com"
@pytest.mark.asyncio
async def test_navigate(self, mock_computer):
"""Test navigate method."""
test_url = "https://test.example.com"
state = await mock_computer.navigate(test_url)
assert isinstance(state, ComputerState)
assert state.url == test_url
@pytest.mark.asyncio
async def test_key_combination(self, mock_computer):
"""Test key_combination method."""
state = await mock_computer.key_combination(["ctrl", "c"])
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_drag_and_drop(self, mock_computer):
"""Test drag_and_drop method."""
state = await mock_computer.drag_and_drop(100, 200, 300, 400)
assert isinstance(state, ComputerState)
@pytest.mark.asyncio
async def test_current_state(self, mock_computer):
"""Test current_state method."""
state = await mock_computer.current_state()
assert isinstance(state, ComputerState)
assert state.url == "https://example.com"
assert state.screenshot == b"screenshot_data"
@pytest.mark.asyncio
async def test_initialize(self, mock_computer):
"""Test initialize method."""
assert not mock_computer.initialized
await mock_computer.initialize()
assert mock_computer.initialized
@pytest.mark.asyncio
async def test_close(self, mock_computer):
"""Test close method."""
assert not mock_computer.closed
await mock_computer.close()
assert mock_computer.closed
@pytest.mark.asyncio
async def test_environment(self, mock_computer):
"""Test environment method."""
env = await mock_computer.environment()
assert env == ComputerEnvironment.ENVIRONMENT_BROWSER
assert isinstance(env, ComputerEnvironment)
@pytest.mark.asyncio
async def test_lifecycle_methods(self, mock_computer):
"""Test the lifecycle of a computer instance."""
# Initially not initialized or closed
assert not mock_computer.initialized
assert not mock_computer.closed
# Initialize
await mock_computer.initialize()
assert mock_computer.initialized
assert not mock_computer.closed
# Close
await mock_computer.close()
assert mock_computer.initialized
assert mock_computer.closed
@@ -0,0 +1,618 @@
# 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 base64
import inspect
from google.adk.agents.invocation_context import InvocationContext
from google.adk.agents.sequential_agent import SequentialAgent
from google.adk.models.llm_request import LlmRequest
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.tools.computer_use.base_computer import ComputerState
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
from google.adk.tools.tool_context import ToolContext
import pytest
class TestComputerUseTool:
"""Test cases for ComputerUseTool class."""
@pytest.fixture
async def tool_context(self):
"""Fixture providing a tool context."""
session_service = InMemorySessionService()
session = await session_service.create_session(
app_name="test_app", user_id="test_user"
)
agent = SequentialAgent(name="test_agent")
invocation_context = InvocationContext(
invocation_id="invocation_id",
agent=agent,
session=session,
session_service=session_service,
)
return ToolContext(invocation_context=invocation_context)
@pytest.fixture
def mock_computer_function(self):
"""Fixture providing a mock computer function."""
# Create a real async function instead of AsyncMock for better test control
calls = []
async def mock_func(*args, **kwargs):
calls.append((args, kwargs))
# Return a default ComputerState - this will be overridden in individual tests
return ComputerState(screenshot=b"default", url="https://default.com")
# Add attributes that tests expect
mock_func.__name__ = "test_function"
mock_func.__doc__ = "Test function documentation"
mock_func.calls = calls
# Add assertion methods for compatibility with Mock
def assert_called_once_with(*args, **kwargs):
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
assert calls[0] == (
args,
kwargs,
), f"Expected {(args, kwargs)}, got {calls[0]}"
def assert_called_once():
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
mock_func.assert_called_once_with = assert_called_once_with
mock_func.assert_called_once = assert_called_once
return mock_func
def test_init(self, mock_computer_function):
"""Test ComputerUseTool initialization."""
screen_size = (1920, 1080)
tool = ComputerUseTool(func=mock_computer_function, screen_size=screen_size)
assert tool._screen_size == screen_size
assert tool.func == mock_computer_function
def test_init_with_invalid_screen_size(self, mock_computer_function):
"""Test ComputerUseTool initialization with invalid screen size."""
with pytest.raises(ValueError, match="screen_size must be a tuple"):
ComputerUseTool(func=mock_computer_function, screen_size=[1920, 1080])
with pytest.raises(ValueError, match="screen_size must be a tuple"):
ComputerUseTool(func=mock_computer_function, screen_size=(1920,))
with pytest.raises(
ValueError, match="screen_size dimensions must be positive"
):
ComputerUseTool(func=mock_computer_function, screen_size=(0, 1080))
with pytest.raises(
ValueError, match="screen_size dimensions must be positive"
):
ComputerUseTool(func=mock_computer_function, screen_size=(1920, -1))
def test_init_with_invalid_virtual_screen_size(self, mock_computer_function):
"""Test ComputerUseTool initialization with invalid virtual_screen_size."""
with pytest.raises(ValueError, match="virtual_screen_size must be a tuple"):
ComputerUseTool(
func=mock_computer_function,
screen_size=(1920, 1080),
virtual_screen_size=[1000, 1000],
)
with pytest.raises(ValueError, match="virtual_screen_size must be a tuple"):
ComputerUseTool(
func=mock_computer_function,
screen_size=(1920, 1080),
virtual_screen_size=(1000,),
)
with pytest.raises(
ValueError, match="virtual_screen_size dimensions must be positive"
):
ComputerUseTool(
func=mock_computer_function,
screen_size=(1920, 1080),
virtual_screen_size=(0, 1000),
)
with pytest.raises(
ValueError, match="virtual_screen_size dimensions must be positive"
):
ComputerUseTool(
func=mock_computer_function,
screen_size=(1920, 1080),
virtual_screen_size=(1000, -1),
)
def test_init_with_custom_virtual_screen_size(self, mock_computer_function):
"""Test ComputerUseTool initialization with custom virtual_screen_size."""
screen_size = (1920, 1080)
virtual_screen_size = (2000, 2000)
tool = ComputerUseTool(
func=mock_computer_function,
screen_size=screen_size,
virtual_screen_size=virtual_screen_size,
)
assert tool._screen_size == screen_size
assert tool._coordinate_space == virtual_screen_size
assert tool.func == mock_computer_function
def test_normalize_x(self, mock_computer_function):
"""Test x coordinate normalization with default virtual screen size (1000x1000)."""
tool = ComputerUseTool(
func=mock_computer_function, screen_size=(1920, 1080)
)
# Test normal cases
assert tool._normalize_x(0) == 0
assert tool._normalize_x(500) == 960 # 500/1000 * 1920
assert tool._normalize_x(1000) == 1919 # Clamped to screen bounds
# Test edge cases
assert tool._normalize_x(-100) == 0 # Clamped to 0
assert tool._normalize_x(1500) == 1919 # Clamped to max
def test_normalize_y(self, mock_computer_function):
"""Test y coordinate normalization with default virtual screen size (1000x1000)."""
tool = ComputerUseTool(
func=mock_computer_function, screen_size=(1920, 1080)
)
# Test normal cases
assert tool._normalize_y(0) == 0
assert tool._normalize_y(500) == 540 # 500/1000 * 1080
assert tool._normalize_y(1000) == 1079 # Clamped to screen bounds
# Test edge cases
assert tool._normalize_y(-100) == 0 # Clamped to 0
assert tool._normalize_y(1500) == 1079 # Clamped to max
def test_normalize_with_custom_virtual_screen_size(
self, mock_computer_function
):
"""Test coordinate normalization with custom virtual screen size."""
tool = ComputerUseTool(
func=mock_computer_function,
screen_size=(1920, 1080),
virtual_screen_size=(2000, 2000),
)
# Test x coordinate normalization with 2000x2000 virtual space
assert tool._normalize_x(0) == 0
assert tool._normalize_x(1000) == 960 # 1000/2000 * 1920
assert tool._normalize_x(2000) == 1919 # Clamped to screen bounds
# Test y coordinate normalization with 2000x2000 virtual space
assert tool._normalize_y(0) == 0
assert tool._normalize_y(1000) == 540 # 1000/2000 * 1080
assert tool._normalize_y(2000) == 1079 # Clamped to screen bounds
# Test edge cases
assert tool._normalize_x(-100) == 0 # Clamped to 0
assert tool._normalize_x(3000) == 1919 # Clamped to max
assert tool._normalize_y(-100) == 0 # Clamped to 0
assert tool._normalize_y(3000) == 1079 # Clamped to max
def test_normalize_with_invalid_coordinates(self, mock_computer_function):
"""Test coordinate normalization with invalid inputs."""
tool = ComputerUseTool(
func=mock_computer_function, screen_size=(1920, 1080)
)
with pytest.raises(ValueError, match="x coordinate must be numeric"):
tool._normalize_x("invalid")
with pytest.raises(ValueError, match="y coordinate must be numeric"):
tool._normalize_y("invalid")
@pytest.mark.asyncio
async def test_run_async_with_coordinates(
self, mock_computer_function, tool_context
):
"""Test run_async with coordinate normalization."""
# Set up a proper signature for the mock function
def dummy_func(x: int, y: int):
pass
mock_computer_function.__name__ = "dummy_func"
mock_computer_function.__signature__ = inspect.signature(dummy_func)
# Create a specific mock function for this test that returns the expected state
calls = []
mock_state = ComputerState(
screenshot=b"test_screenshot", url="https://example.com"
)
async def specific_mock_func(x: int, y: int):
calls.append((x, y))
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
def assert_called_once_with(x, y):
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
assert calls[0] == (x, y), f"Expected ({x}, {y}), got {calls[0]}"
specific_mock_func.assert_called_once_with = assert_called_once_with
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
args = {"x": 500, "y": 300}
result = await tool.run_async(args=args, tool_context=tool_context)
# Check that coordinates were normalized
specific_mock_func.assert_called_once_with(x=960, y=324)
# Check return format for ComputerState
expected_result = {
"image": {
"mimetype": "image/png",
"data": base64.b64encode(b"test_screenshot").decode("utf-8"),
},
"url": "https://example.com",
}
assert result == expected_result
@pytest.mark.asyncio
async def test_run_async_with_drag_and_drop_coordinates(
self, mock_computer_function, tool_context
):
"""Test run_async with drag and drop coordinate normalization."""
# Set up a proper signature for the mock function
def dummy_func(x: int, y: int, destination_x: int, destination_y: int):
pass
# Create a specific mock function for this test
calls = []
mock_state = ComputerState(
screenshot=b"test_screenshot", url="https://example.com"
)
async def specific_mock_func(
x: int, y: int, destination_x: int, destination_y: int
):
calls.append((x, y, destination_x, destination_y))
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
def assert_called_once_with(x, y, destination_x, destination_y):
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
assert calls[0] == (x, y, destination_x, destination_y), (
f"Expected ({x}, {y}, {destination_x}, {destination_y}), got"
f" {calls[0]}"
)
specific_mock_func.assert_called_once_with = assert_called_once_with
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
args = {"x": 100, "y": 200, "destination_x": 800, "destination_y": 600}
result = await tool.run_async(args=args, tool_context=tool_context)
# Check that all coordinates were normalized
specific_mock_func.assert_called_once_with(
x=192, # 100/1000 * 1920
y=216, # 200/1000 * 1080
destination_x=1536, # 800/1000 * 1920
destination_y=648, # 600/1000 * 1080
)
@pytest.mark.asyncio
async def test_run_async_with_non_computer_state_result(
self, mock_computer_function, tool_context
):
"""Test run_async when function returns non-ComputerState result."""
# Create a specific mock function that returns non-ComputerState
calls = []
async def specific_mock_func(*args, **kwargs):
calls.append((args, kwargs))
return {"status": "success"}
specific_mock_func.__name__ = "test_function"
specific_mock_func.calls = calls
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
args = {"text": "hello"}
result = await tool.run_async(args=args, tool_context=tool_context)
# Should return the result as-is
assert result == {"status": "success"}
@pytest.mark.asyncio
async def test_run_async_without_coordinates(
self, mock_computer_function, tool_context
):
"""Test run_async with no coordinate parameters."""
# Set up a proper signature for the mock function
def dummy_func(direction: str):
pass
# Create a specific mock function for this test
calls = []
mock_state = ComputerState(
screenshot=b"test_screenshot", url="https://example.com"
)
async def specific_mock_func(direction: str):
calls.append((direction,))
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
def assert_called_once_with(direction):
assert len(calls) == 1, f"Expected 1 call, got {len(calls)}"
assert calls[0] == (
direction,
), f"Expected ({direction},), got {calls[0]}"
specific_mock_func.assert_called_once_with = assert_called_once_with
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
args = {"direction": "down"}
result = await tool.run_async(args=args, tool_context=tool_context)
# Should call function with original args
specific_mock_func.assert_called_once_with(direction="down")
@pytest.mark.asyncio
async def test_run_async_with_error(
self, mock_computer_function, tool_context
):
"""Test run_async when underlying function raises an error."""
# Create a specific mock function that raises an error
calls = []
async def specific_mock_func(*args, **kwargs):
calls.append((args, kwargs))
raise ValueError("Test error")
specific_mock_func.__name__ = "test_function"
specific_mock_func.calls = calls
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
args = {"x": 500, "y": 300}
with pytest.raises(ValueError, match="Test error"):
await tool.run_async(args=args, tool_context=tool_context)
@pytest.mark.asyncio
async def test_process_llm_request(
self, mock_computer_function, tool_context
):
"""Test process_llm_request method."""
tool = ComputerUseTool(
func=mock_computer_function, screen_size=(1920, 1080)
)
llm_request = LlmRequest()
# Should not raise any exceptions and should do nothing
await tool.process_llm_request(
tool_context=tool_context, llm_request=llm_request
)
# Verify llm_request is unchanged (process_llm_request is now a no-op)
assert llm_request.tools_dict == {}
@pytest.mark.asyncio
async def test_run_async_with_safety_confirmation(
self, mock_computer_function, tool_context
):
"""Test run_async with safety confirmation."""
from google.adk.tools.tool_confirmation import ToolConfirmation
# Set up a proper signature for the mock function
def dummy_func():
pass
# Create a specific mock function for this test
calls = []
mock_state = ComputerState(
screenshot=b"test_screenshot", url="https://example.com"
)
async def specific_mock_func():
calls.append(())
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
tool_context.function_call_id = "test_fc_id"
# Test case 1: require confirmation, not yet confirmed
args = {"safety_decision": {"decision": "require_confirmation"}}
result = await tool.run_async(args=args, tool_context=tool_context)
# Check that confirmation was requested
assert "test_fc_id" in tool_context.actions.requested_tool_confirmations
assert (
tool_context.actions.requested_tool_confirmations["test_fc_id"].hint
== "This computer use action requires safety confirmation."
)
assert result == {
"error": (
"This tool call requires confirmation, please approve or reject."
)
}
assert len(calls) == 0
# Test case 2: confirmed
tool_context.tool_confirmation = ToolConfirmation(confirmed=True)
result = await tool.run_async(args=args, tool_context=tool_context)
# Should execute normally
assert len(calls) == 1
assert result == {
"image": {
"mimetype": "image/png",
"data": base64.b64encode(b"test_screenshot").decode("utf-8"),
},
"url": "https://example.com",
"safety_acknowledgement": "true",
}
# Test case 3: rejected
tool_context.tool_confirmation = ToolConfirmation(confirmed=False)
result = await tool.run_async(args=args, tool_context=tool_context)
assert result == {"error": "This tool call is rejected."}
@pytest.mark.asyncio
async def test_run_async_with_safety_decision_dict(
self, mock_computer_function, tool_context
):
"""Test run_async with safety decision as a dictionary."""
from google.adk.tools.tool_confirmation import ToolConfirmation
# Set up a proper signature for the mock function
def dummy_func():
pass
calls = []
mock_state = ComputerState(screenshot=b"test", url="https://example.com")
async def specific_mock_func():
calls.append(())
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
tool_context.function_call_id = "test_fc_id_dict"
args = {
"safety_decision": {
"explanation": (
"I need you to complete the challenge by clicking the 'I'm not"
" a robot' checkbox."
),
"decision": "require_confirmation",
}
}
result = await tool.run_async(args=args, tool_context=tool_context)
assert (
"test_fc_id_dict" in tool_context.actions.requested_tool_confirmations
)
assert (
tool_context.actions.requested_tool_confirmations[
"test_fc_id_dict"
].hint
== "I need you to complete the challenge by clicking the 'I'm not a"
" robot' checkbox."
)
assert result == {
"error": (
"This tool call requires confirmation, please approve or reject."
)
}
assert len(calls) == 0
def test_inheritance(self, mock_computer_function):
"""Test that ComputerUseTool inherits from FunctionTool."""
from google.adk.tools.function_tool import FunctionTool
tool = ComputerUseTool(
func=mock_computer_function, screen_size=(1920, 1080)
)
assert isinstance(tool, FunctionTool)
def test_custom_screen_size(self, mock_computer_function):
"""Test ComputerUseTool with custom screen size and default virtual screen size."""
custom_size = (2560, 1440)
tool = ComputerUseTool(func=mock_computer_function, screen_size=custom_size)
# Test normalization with custom screen size and default 1000x1000 virtual space
assert tool._normalize_x(500) == 1280 # 500/1000 * 2560
assert tool._normalize_y(500) == 720 # 500/1000 * 1440
def test_custom_screen_size_with_custom_virtual_screen_size(
self, mock_computer_function
):
"""Test ComputerUseTool with both custom screen size and custom virtual screen size."""
screen_size = (2560, 1440)
virtual_screen_size = (800, 600)
tool = ComputerUseTool(
func=mock_computer_function,
screen_size=screen_size,
virtual_screen_size=virtual_screen_size,
)
# Test normalization: 400/800 * 2560 = 1280, 300/600 * 1440 = 720
assert tool._normalize_x(400) == 1280 # 400/800 * 2560
assert tool._normalize_y(300) == 720 # 300/600 * 1440
# Test bounds
assert (
tool._normalize_x(800) == 2559
) # 800/800 * 2560, clamped to screen bounds
assert (
tool._normalize_y(600) == 1439
) # 600/600 * 1440, clamped to screen bounds
@pytest.mark.asyncio
async def test_coordinate_logging(
self, mock_computer_function, tool_context, caplog
):
"""Test that coordinate normalization is logged."""
import logging
# Set up a proper signature for the mock function
def dummy_func(x: int, y: int):
pass
# Create a specific mock function for this test
calls = []
mock_state = ComputerState(
screenshot=b"test_screenshot", url="https://example.com"
)
async def specific_mock_func(x: int, y: int):
calls.append((x, y))
return mock_state
specific_mock_func.__name__ = "dummy_func"
specific_mock_func.__signature__ = inspect.signature(dummy_func)
specific_mock_func.calls = calls
tool = ComputerUseTool(func=specific_mock_func, screen_size=(1920, 1080))
# Set the specific logger used by ComputerUseTool to DEBUG level
logger_name = "google_adk.google.adk.tools.computer_use.computer_use_tool"
with caplog.at_level(logging.DEBUG, logger=logger_name):
args = {"x": 500, "y": 300}
await tool.run_async(args=args, tool_context=tool_context)
# Check that normalization was logged
assert "Normalized x: 500 -> 960" in caplog.text
assert "Normalized y: 300 -> 324" in caplog.text
@@ -0,0 +1,612 @@
# 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 AsyncMock
from unittest.mock import MagicMock
from google.adk.models.llm_request import LlmRequest
# Use the actual ComputerEnvironment enum from the code
from google.adk.tools.computer_use.base_computer import BaseComputer
from google.adk.tools.computer_use.base_computer import ComputerEnvironment
from google.adk.tools.computer_use.base_computer import ComputerState
from google.adk.tools.computer_use.computer_use_tool import ComputerUseTool
from google.adk.tools.computer_use.computer_use_toolset import ComputerUseToolset
from google.genai import types
import pytest
class MockComputer(BaseComputer):
"""Mock Computer implementation for testing."""
def __init__(self):
self.initialize_called = False
self.close_called = False
self._screen_size = (1920, 1080)
self._environment = ComputerEnvironment.ENVIRONMENT_BROWSER
async def initialize(self):
self.initialize_called = True
async def close(self):
self.close_called = True
async def screen_size(self) -> tuple[int, int]:
return self._screen_size
async def environment(self) -> ComputerEnvironment:
return self._environment
# Implement all abstract methods to make this a concrete class
async def open_web_browser(self) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def click_at(self, x: int, y: int) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def hover_at(self, x: int, y: int) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def type_text_at(
self,
x: int,
y: int,
text: str,
press_enter: bool = True,
clear_before_typing: bool = True,
) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def scroll_document(self, direction: str) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def scroll_at(
self, x: int, y: int, direction: str, magnitude: int
) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def wait(self, seconds: int) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def go_back(self) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def go_forward(self) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def search(self) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def navigate(self, url: str) -> ComputerState:
return ComputerState(screenshot=b"test", url=url)
async def key_combination(self, keys: list[str]) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def drag_and_drop(
self, x: int, y: int, destination_x: int, destination_y: int
) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
async def current_state(self) -> ComputerState:
return ComputerState(screenshot=b"test", url="https://example.com")
class TestComputerUseToolset:
"""Test cases for ComputerUseToolset class."""
@pytest.fixture
def mock_computer(self):
"""Fixture providing a mock computer."""
return MockComputer()
@pytest.fixture
def toolset(self, mock_computer):
"""Fixture providing a ComputerUseToolset instance."""
return ComputerUseToolset(computer=mock_computer)
def test_init(self, mock_computer):
"""Test ComputerUseToolset initialization."""
toolset = ComputerUseToolset(computer=mock_computer)
assert toolset._computer == mock_computer
assert toolset._initialized is False
@pytest.mark.asyncio
async def test_ensure_initialized(self, toolset, mock_computer):
"""Test that _ensure_initialized calls computer.initialize()."""
assert not mock_computer.initialize_called
assert not toolset._initialized
await toolset._ensure_initialized()
assert mock_computer.initialize_called
assert toolset._initialized
@pytest.mark.asyncio
async def test_ensure_initialized_only_once(self, toolset, mock_computer):
"""Test that _ensure_initialized only calls initialize once."""
await toolset._ensure_initialized()
# Reset the flag to test it's not called again
mock_computer.initialize_called = False
await toolset._ensure_initialized()
# Should not be called again
assert not mock_computer.initialize_called
assert toolset._initialized
@pytest.mark.asyncio
async def test_get_tools(self, toolset, mock_computer):
"""Test that get_tools returns ComputerUseTool instances."""
tools = await toolset.get_tools()
# Should initialize the computer
assert mock_computer.initialize_called
# Should return a list of ComputerUseTool instances
assert isinstance(tools, list)
assert len(tools) > 0
assert all(isinstance(tool, ComputerUseTool) for tool in tools)
# Each tool should have the correct configuration
for tool in tools:
assert tool._screen_size == (1920, 1080)
# Should use default virtual screen size
assert tool._coordinate_space == (1000, 1000)
@pytest.mark.asyncio
async def test_get_tools_excludes_utility_methods(self, toolset):
"""Test that get_tools excludes utility methods like screen_size, environment, close."""
tools = await toolset.get_tools()
# Get tool function names
tool_names = [tool.func.__name__ for tool in tools]
# Should exclude utility methods
excluded_methods = {"screen_size", "environment", "close"}
for method in excluded_methods:
assert method not in tool_names
# initialize might be included since it's a concrete method, not just abstract
# This is acceptable behavior
# Should include action methods
expected_methods = {
"open_web_browser",
"click_at",
"hover_at",
"type_text_at",
"scroll_document",
"scroll_at",
"wait",
"go_back",
"go_forward",
"search",
"navigate",
"key_combination",
"drag_and_drop",
"current_state",
}
for method in expected_methods:
assert method in tool_names
@pytest.mark.asyncio
async def test_get_tools_filters_excluded_functions(self, mock_computer):
"""Test that get_tools filters out excluded functions."""
excluded_funcs = ["drag_and_drop", "key_combination"]
toolset = ComputerUseToolset(
computer=mock_computer,
excluded_predefined_functions=excluded_funcs,
)
tools = await toolset.get_tools()
tool_names = [tool.func.__name__ for tool in tools]
for func in excluded_funcs:
assert func not in tool_names
assert "click_at" in tool_names
@pytest.mark.asyncio
async def test_get_tools_with_readonly_context(self, toolset):
"""Test get_tools with readonly_context parameter."""
from google.adk.agents.readonly_context import ReadonlyContext
readonly_context = MagicMock(spec=ReadonlyContext)
tools = await toolset.get_tools(readonly_context=readonly_context)
# Should still return tools (readonly_context doesn't affect behavior currently)
assert isinstance(tools, list)
assert len(tools) > 0
@pytest.mark.asyncio
async def test_close(self, toolset, mock_computer):
"""Test that close calls computer.close()."""
await toolset.close()
assert mock_computer.close_called
@pytest.mark.asyncio
async def test_get_tools_creates_tools_with_correct_methods(
self, toolset, mock_computer
):
"""Test that get_tools creates tools with the correct underlying methods."""
tools = await toolset.get_tools()
# Find the click_at tool
click_tool = None
for tool in tools:
if tool.func.__name__ == "click_at":
click_tool = tool
break
assert click_tool is not None
# The tool's function should have the correct name (wrapped method)
assert click_tool.func.__name__ == "click_at"
@pytest.mark.asyncio
async def test_get_tools_handles_custom_screen_size(self, mock_computer):
"""Test get_tools with custom screen size."""
mock_computer._screen_size = (2560, 1440)
toolset = ComputerUseToolset(computer=mock_computer)
tools = await toolset.get_tools()
# All tools should have the custom screen size
for tool in tools:
assert tool._screen_size == (2560, 1440)
@pytest.mark.asyncio
async def test_get_tools_handles_custom_environment(self, mock_computer):
"""Test get_tools with custom environment."""
mock_computer._environment = ComputerEnvironment.ENVIRONMENT_UNSPECIFIED
toolset = ComputerUseToolset(computer=mock_computer)
tools = await toolset.get_tools()
# Should still return tools regardless of environment
assert isinstance(tools, list)
assert len(tools) > 0
@pytest.mark.asyncio
async def test_multiple_get_tools_calls_return_cached_instances(
self, toolset
):
"""Test that multiple get_tools calls return the same cached instances."""
tools1 = await toolset.get_tools()
tools2 = await toolset.get_tools()
# Should return the same list instance
assert tools1 is tools2
def test_inheritance(self, toolset):
"""Test that ComputerUseToolset inherits from BaseToolset."""
from google.adk.tools.base_toolset import BaseToolset
assert isinstance(toolset, BaseToolset)
@pytest.mark.asyncio
async def test_get_tools_method_filtering(self, toolset):
"""Test that get_tools properly filters methods from BaseComputer."""
tools = await toolset.get_tools()
# Get all method names from the tools
tool_method_names = [tool.func.__name__ for tool in tools]
# Should not include private methods (starting with _)
for name in tool_method_names:
assert not name.startswith("_")
# Should not include excluded methods
excluded_methods = {"screen_size", "environment", "close"}
for excluded in excluded_methods:
assert excluded not in tool_method_names
@pytest.mark.asyncio
async def test_computer_method_binding(self, toolset, mock_computer):
"""Test that tools are properly bound to the computer instance."""
tools = await toolset.get_tools()
# All tools should have wrapped functions with correct names
for tool in tools:
# Wrapped functions preserve the original method name via functools.wraps
assert callable(tool.func)
assert not tool.func.__name__.startswith("_")
@pytest.mark.asyncio
async def test_toolset_handles_computer_initialization_failure(
self, mock_computer
):
"""Test that toolset handles computer initialization failure gracefully."""
# Make initialize raise an exception
async def failing_initialize():
raise Exception("Initialization failed")
mock_computer.initialize = failing_initialize
toolset = ComputerUseToolset(computer=mock_computer)
# Should raise the exception when trying to get tools
with pytest.raises(Exception, match="Initialization failed"):
await toolset.get_tools()
@pytest.mark.asyncio
async def test_process_llm_request(self, toolset, mock_computer):
"""Test that process_llm_request adds tools and computer use configuration."""
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
await toolset.process_llm_request(
tool_context=MagicMock(), llm_request=llm_request
)
# Should add tools to the request
assert len(llm_request.tools_dict) > 0
# Should add computer use configuration
assert llm_request.config.tools is not None
assert len(llm_request.config.tools) > 0
# Should have computer use tool
computer_use_tools = [
tool
for tool in llm_request.config.tools
if hasattr(tool, "computer_use") and tool.computer_use
]
assert len(computer_use_tools) == 1
# Should have correct environment
computer_use_tool = computer_use_tools[0]
assert (
computer_use_tool.computer_use.environment
== types.Environment.ENVIRONMENT_BROWSER
)
@pytest.mark.asyncio
async def test_process_llm_request_with_excluded_functions(
self, mock_computer
):
"""Test that process_llm_request passes excluded_predefined_functions."""
excluded_funcs = ["drag_and_drop", "key_combination"]
toolset = ComputerUseToolset(
computer=mock_computer,
excluded_predefined_functions=excluded_funcs,
)
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
await toolset.process_llm_request(
tool_context=MagicMock(), llm_request=llm_request
)
# Should have computer use tool
computer_use_tools = [
tool
for tool in llm_request.config.tools
if hasattr(tool, "computer_use") and tool.computer_use
]
assert len(computer_use_tools) == 1
# Should have correct excluded functions
computer_use_tool = computer_use_tools[0]
assert (
computer_use_tool.computer_use.excluded_predefined_functions
== excluded_funcs
)
@pytest.mark.asyncio
async def test_process_llm_request_with_existing_computer_use(
self, toolset, mock_computer
):
"""Test that process_llm_request doesn't add duplicate computer use configuration."""
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(
tools=[
types.Tool(
computer_use=types.ComputerUse(
environment=types.Environment.ENVIRONMENT_BROWSER
)
)
]
),
)
original_tools_count = len(llm_request.config.tools)
await toolset.process_llm_request(
tool_context=MagicMock(), llm_request=llm_request
)
# Should not add duplicate computer use configuration
assert len(llm_request.config.tools) == original_tools_count
# Should still add the actual tools
assert len(llm_request.tools_dict) > 0
@pytest.mark.asyncio
async def test_process_llm_request_error_handling(self, mock_computer):
"""Test that process_llm_request handles errors gracefully."""
# Make environment raise an exception
async def failing_environment():
raise Exception("Environment failed")
mock_computer.environment = failing_environment
toolset = ComputerUseToolset(computer=mock_computer)
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
# Should raise the exception
with pytest.raises(Exception, match="Environment failed"):
await toolset.process_llm_request(
tool_context=MagicMock(), llm_request=llm_request
)
@pytest.mark.asyncio
async def test_adapt_computer_use_tool_sync_adapter(self):
"""Test adapt_computer_use_tool with sync adapter function."""
# Create a mock tool
mock_func = AsyncMock()
original_tool = ComputerUseTool(
func=mock_func,
screen_size=(1920, 1080),
virtual_screen_size=(1000, 1000),
)
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
llm_request.tools_dict["wait"] = original_tool
# Create a sync adapter function
def sync_adapter(original_func):
async def adapted_func():
return await original_func(5)
return adapted_func
# Call the adaptation method
await ComputerUseToolset.adapt_computer_use_tool(
"wait", sync_adapter, llm_request
)
# Verify the original tool was replaced
assert "wait" not in llm_request.tools_dict
assert "adapted_func" in llm_request.tools_dict
# Verify the new tool has correct properties
adapted_tool = llm_request.tools_dict["adapted_func"]
assert isinstance(adapted_tool, ComputerUseTool)
assert adapted_tool._screen_size == (1920, 1080)
assert adapted_tool._coordinate_space == (1000, 1000)
@pytest.mark.asyncio
async def test_adapt_computer_use_tool_async_adapter(self):
"""Test adapt_computer_use_tool with async adapter function."""
# Create a mock tool
mock_func = AsyncMock()
original_tool = ComputerUseTool(
func=mock_func,
screen_size=(1920, 1080),
virtual_screen_size=(1000, 1000),
)
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
llm_request.tools_dict["wait"] = original_tool
# Create an async adapter function
async def async_adapter(original_func):
async def adapted_func():
return await original_func(5)
return adapted_func
# Call the adaptation method
await ComputerUseToolset.adapt_computer_use_tool(
"wait", async_adapter, llm_request
)
# Verify the original tool was replaced
assert "wait" not in llm_request.tools_dict
assert "adapted_func" in llm_request.tools_dict
# Verify the new tool has correct properties
adapted_tool = llm_request.tools_dict["adapted_func"]
assert isinstance(adapted_tool, ComputerUseTool)
assert adapted_tool._screen_size == (1920, 1080)
assert adapted_tool._coordinate_space == (1000, 1000)
@pytest.mark.asyncio
async def test_adapt_computer_use_tool_invalid_method(self):
"""Test adapt_computer_use_tool with invalid method name."""
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
def adapter(original_func):
async def adapted_func():
return await original_func()
return adapted_func
# Should not raise an exception, just log a warning
await ComputerUseToolset.adapt_computer_use_tool(
"invalid_method", adapter, llm_request
)
# Should not add any tools
assert len(llm_request.tools_dict) == 0
@pytest.mark.asyncio
async def test_adapt_computer_use_tool_excluded_method(self):
"""Test adapt_computer_use_tool with excluded method name."""
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
def adapter(original_func):
async def adapted_func():
return await original_func()
return adapted_func
# Should not raise an exception, just log a warning
await ComputerUseToolset.adapt_computer_use_tool(
"screen_size", adapter, llm_request
)
# Should not add any tools
assert len(llm_request.tools_dict) == 0
@pytest.mark.asyncio
async def test_adapt_computer_use_tool_method_not_in_tools_dict(self):
"""Test adapt_computer_use_tool when method is not in tools_dict."""
llm_request = LlmRequest(
model="gemini-2.5-flash",
config=types.GenerateContentConfig(),
)
def adapter(original_func):
async def adapted_func():
return await original_func()
return adapted_func
# Should not raise an exception, just log a warning
await ComputerUseToolset.adapt_computer_use_tool(
"wait", adapter, llm_request
)
# Should not add any tools
assert len(llm_request.tools_dict) == 0