chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
"""Pytest configuration and shared fixtures for agent package tests.
This file contains shared fixtures and configuration for all agent tests.
Following SRP: This file ONLY handles test setup/teardown.
"""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
@pytest.fixture
def mock_litellm():
"""Mock liteLLM completion calls.
Use this fixture to avoid making real LLM API calls during tests.
Returns a mock that simulates LLM responses.
"""
with patch("litellm.acompletion") as mock_completion:
async def mock_response(*args, **kwargs):
"""Simulate a typical LLM response."""
return {
"id": "chatcmpl-test123",
"object": "chat.completion",
"created": 1234567890,
"model": kwargs.get("model", "anthropic/claude-sonnet-4-5-20250929"),
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "This is a mocked response for testing.",
},
"finish_reason": "stop",
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 20,
"total_tokens": 30,
},
}
mock_completion.side_effect = mock_response
yield mock_completion
@pytest.fixture
def mock_computer():
"""Mock Computer interface for agent tests.
Use this fixture to test agent logic without requiring a real Computer instance.
"""
computer = AsyncMock()
computer.interface = AsyncMock()
computer.interface.screenshot = AsyncMock(return_value=b"fake_screenshot_data")
computer.interface.left_click = AsyncMock()
computer.interface.type = AsyncMock()
computer.interface.key = AsyncMock()
# Mock context manager
computer.__aenter__ = AsyncMock(return_value=computer)
computer.__aexit__ = AsyncMock()
return computer
@pytest.fixture
def disable_telemetry(monkeypatch):
"""Disable telemetry for tests.
Use this fixture to ensure no telemetry is sent during tests.
"""
monkeypatch.setenv("CUA_TELEMETRY_ENABLED", "false")
@pytest.fixture
def sample_messages():
"""Provide sample messages for testing.
Returns a list of messages in the expected format.
"""
return [{"role": "user", "content": "Take a screenshot and tell me what you see"}]
@@ -0,0 +1,139 @@
"""Unit tests for ComputerAgent class.
This file tests ONLY the ComputerAgent initialization and basic functionality.
Following SRP: This file tests ONE class (ComputerAgent).
All external dependencies (liteLLM, Computer) are mocked.
"""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
class TestComputerAgentInitialization:
"""Test ComputerAgent initialization (SRP: Only tests initialization)."""
@patch("cua_agent.agent.litellm")
def test_agent_initialization_with_model(self, mock_litellm, disable_telemetry):
"""Test that agent can be initialized with a model string."""
from cua_agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
assert agent is not None
assert hasattr(agent, "model")
assert agent.model == "anthropic/claude-sonnet-4-5-20250929"
@patch("cua_agent.agent.litellm")
def test_agent_initialization_with_tools(self, mock_litellm, disable_telemetry, mock_computer):
"""Test that agent can be initialized with tools."""
from cua_agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929", tools=[mock_computer])
assert agent is not None
assert hasattr(agent, "tools")
@patch("cua_agent.agent.litellm")
def test_agent_initialization_with_max_budget(self, mock_litellm, disable_telemetry):
"""Test that agent can be initialized with max trajectory budget."""
from cua_agent import ComputerAgent
budget = 5.0
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929", max_trajectory_budget=budget
)
assert agent is not None
@patch("cua_agent.agent.litellm")
def test_agent_requires_model(self, mock_litellm, disable_telemetry):
"""Test that agent requires a model parameter."""
from cua_agent import ComputerAgent
with pytest.raises(TypeError):
# Should fail without model parameter - intentionally missing required argument
ComputerAgent() # type: ignore[call-arg]
class TestComputerAgentRun:
"""Test ComputerAgent.run() method (SRP: Only tests run logic)."""
@pytest.mark.asyncio
@patch("cua_agent.agent.litellm")
async def test_agent_run_with_messages(self, mock_litellm, disable_telemetry, sample_messages):
"""Test that agent.run() works with valid messages."""
from cua_agent import ComputerAgent
# Mock liteLLM response
mock_response = {
"id": "chatcmpl-test",
"choices": [
{
"message": {"role": "assistant", "content": "Test response"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
}
mock_litellm.acompletion = AsyncMock(return_value=mock_response)
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
# Run should return an async generator
result_generator = agent.run(sample_messages)
assert result_generator is not None
# Check it's an async generator
assert hasattr(result_generator, "__anext__")
def test_agent_has_run_method(self, disable_telemetry):
"""Test that agent has run method available."""
from cua_agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
# Verify run method exists
assert hasattr(agent, "run")
assert callable(agent.run)
def test_agent_has_agent_loop(self, disable_telemetry):
"""Test that agent has agent_loop initialized."""
from cua_agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
# Verify agent_loop is initialized
assert hasattr(agent, "agent_loop")
assert agent.agent_loop is not None
class TestComputerAgentTypes:
"""Test AgentResponse and Messages types (SRP: Only tests type definitions)."""
def test_messages_type_exists(self):
"""Test that Messages type is exported."""
from cua_agent import Messages
assert Messages is not None
def test_agent_response_type_exists(self):
"""Test that AgentResponse type is exported."""
from cua_agent import AgentResponse
assert AgentResponse is not None
class TestComputerAgentIntegration:
"""Test ComputerAgent integration with Computer tool (SRP: Integration within package)."""
def test_agent_accepts_computer_tool(self, disable_telemetry, mock_computer):
"""Test that agent can be initialized with Computer tool."""
from cua_agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929", tools=[mock_computer])
# Verify agent accepted the tool
assert agent is not None
assert hasattr(agent, "tools")
@@ -0,0 +1,96 @@
"""Tests for predict_click zero-coordinate fix (issue #1400).
These tests verify the coordinate-extraction logic in
AnthropicHostedToolsConfig.predict_click directly, without requiring the full
cua_agent import chain (which needs cua-computer, cua-core, etc.).
"""
import pytest
def _extract_click_coords_old(responses_items):
"""Buggy implementation: falsy check silently drops x=0 or y=0."""
for item in responses_items:
if (
isinstance(item, dict)
and item.get("type") == "computer_call"
and isinstance(item.get("action"), dict)
):
action = item["action"]
if action.get("x") and action.get("y"): # BUG: 0 is falsy
return (int(action.get("x")), int(action.get("y")))
return None
def _extract_click_coords_fixed(responses_items):
"""Fixed implementation: explicit None check preserves zero coordinates."""
for item in responses_items:
if (
isinstance(item, dict)
and item.get("type") == "computer_call"
and isinstance(item.get("action"), dict)
):
action = item["action"]
if action.get("x") is not None and action.get("y") is not None:
return (int(action.get("x")), int(action.get("y")))
return None
def _make_items(x, y):
return [{"type": "computer_call", "action": {"type": "click", "x": x, "y": y}}]
# --- Regression tests: these all FAIL with the old code, PASS with the fix ---
def test_zero_x_was_broken_before_fix():
"""Old code returns None for x=0; new code returns (0, y)."""
items = _make_items(0, 100)
assert _extract_click_coords_old(items) is None, "confirm old bug"
assert _extract_click_coords_fixed(items) == (0, 100)
def test_zero_y_was_broken_before_fix():
"""Old code returns None for y=0; new code returns (x, 0)."""
items = _make_items(200, 0)
assert _extract_click_coords_old(items) is None, "confirm old bug"
assert _extract_click_coords_fixed(items) == (200, 0)
def test_zero_zero_was_broken_before_fix():
"""Old code returns None for (0, 0); new code returns (0, 0)."""
items = _make_items(0, 0)
assert _extract_click_coords_old(items) is None, "confirm old bug"
assert _extract_click_coords_fixed(items) == (0, 0)
# --- Positive tests: non-zero coordinates work in both old and new code ---
def test_nonzero_coordinates_still_work():
items = _make_items(512, 384)
assert _extract_click_coords_fixed(items) == (512, 384)
def test_returns_none_when_no_computer_call():
items = [{"type": "text", "text": "no click"}]
assert _extract_click_coords_fixed(items) is None
# --- Verify the actual fix is present in the source file ---
def test_source_uses_is_not_none_check():
"""Confirm the fix is applied in the real anthropic.py source."""
import pathlib
src = (
pathlib.Path(__file__).parent.parent / "cua_agent" / "loops" / "anthropic.py"
).read_text()
assert (
'action.get("x") is not None and action.get("y") is not None' in src
), "Fix not found in anthropic.py — the 'is not None' check is missing"
# Ensure the old buggy pattern is gone
assert (
'if action.get("x") and action.get("y"):' not in src
), "Old buggy truthiness check still present in anthropic.py"
@@ -0,0 +1,145 @@
"""
Test script to verify telemetry events are emitted correctly.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
class TestAgentTelemetryEvents:
"""Test telemetry events emitted by ComputerAgent."""
@patch("cua_agent.agent.record_event")
@patch("cua_agent.agent.is_telemetry_enabled", return_value=True)
def test_agent_init_event(self, mock_telemetry_enabled, mock_record_event):
"""Test that agent_init event is emitted with correct args_provided."""
from cua_agent.agent import ComputerAgent
# Create agent with various args
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
instructions="Test instructions",
max_retries=5, # non-default
trajectory_dir="/tmp/test",
)
# Find the agent_init call
agent_init_calls = [
call for call in mock_record_event.call_args_list if call[0][0] == "agent_init"
]
assert len(agent_init_calls) == 1, "agent_init should be called once"
event_name, event_data = agent_init_calls[0][0]
assert event_name == "agent_init"
assert event_data["model"] == "anthropic/claude-sonnet-4-5-20250929"
assert "instructions" in event_data["args_provided"]
assert "max_retries" in event_data["args_provided"]
assert "trajectory_dir" in event_data["args_provided"]
@patch("cua_agent.agent.record_event")
@patch("cua_agent.agent.is_telemetry_enabled", return_value=True)
def test_agent_init_minimal_args(self, mock_telemetry_enabled, mock_record_event):
"""Test agent_init with minimal args (defaults)."""
from cua_agent.agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
agent_init_calls = [
call for call in mock_record_event.call_args_list if call[0][0] == "agent_init"
]
assert len(agent_init_calls) == 1
event_name, event_data = agent_init_calls[0][0]
# With defaults, only model-related things should be tracked
# instructions, trajectory_dir, etc. should NOT be in args_provided
assert "instructions" not in event_data["args_provided"]
assert "trajectory_dir" not in event_data["args_provided"]
assert "max_retries" not in event_data["args_provided"] # default is 3
@patch("cua_agent.agent.record_event")
@patch("cua_agent.agent.is_telemetry_enabled", return_value=False)
def test_no_events_when_telemetry_disabled(self, mock_telemetry_enabled, mock_record_event):
"""Test that no events are emitted when telemetry is disabled."""
from cua_agent.agent import ComputerAgent
agent = ComputerAgent(
model="anthropic/claude-sonnet-4-5-20250929",
telemetry_enabled=False,
)
# No agent_init should be called (telemetry disabled)
agent_init_calls = [
call for call in mock_record_event.call_args_list if call[0][0] == "agent_init"
]
assert len(agent_init_calls) == 0
class TestActionTelemetryEvents:
"""Test telemetry events for computer actions."""
@pytest.mark.asyncio
@patch("cua_agent.agent.record_event")
@patch("cua_agent.agent.is_telemetry_enabled", return_value=True)
async def test_computer_action_executed_event(self, mock_telemetry_enabled, mock_record_event):
"""Test that computer_action_executed is emitted for computer calls."""
from cua_agent.agent import ComputerAgent
agent = ComputerAgent(model="anthropic/claude-sonnet-4-5-20250929")
agent.telemetry_enabled = True
# Mock computer handler
mock_computer = MagicMock()
mock_computer.click = AsyncMock(return_value=None)
mock_computer.screenshot = AsyncMock(return_value="base64screenshot")
# Create a mock computer_call item
item = {
"type": "computer_call",
"call_id": "test-call-id",
"action": {
"type": "click",
"x": 100,
"y": 200,
},
}
# Process the item (this would normally happen in the agent loop)
# Note: We can't easily test this without running the full agent loop
# This is more of an integration test
# For unit testing, we verify the event structure
expected_event = {
"action_type": "click",
}
# Verify event structure is correct
assert "action_type" in expected_event
class TestToolExecutedEvents:
"""Test telemetry events for tool execution."""
def test_event_structure(self):
"""Test that agent_tool_executed event has correct structure."""
expected_computer_tool_event = {
"tool_type": "computer",
"tool_name": "click",
}
expected_function_tool_event = {
"tool_type": "function",
"tool_name": "my_custom_function",
}
# Verify expected structure
assert "tool_type" in expected_computer_tool_event
assert "tool_name" in expected_computer_tool_event
assert expected_computer_tool_event["tool_type"] in ["computer", "function"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])
@@ -0,0 +1,262 @@
"""
Tests for centralized tool resolution in ComputerAgent.
Tests that:
1. FARA (specialized model) auto-wraps Computer to BrowserTool with warning
2. FARA accepts explicit BrowserTool without warning
3. Claude (general model) accepts any tool without wrapping
4. Custom function tools pass through unchanged
"""
import warnings
from unittest.mock import MagicMock, Mock, patch
import pytest
from cua_agent.computers import is_agent_computer
from cua_agent.tools.browser_tool import BrowserTool
from cua_agent.types import AgentConfigInfo
# Mock agent config class for testing
class MockAgentConfig:
"""Mock agent config class for testing"""
async def predict_step(self, **kwargs):
return {"output": [], "usage": {}}
async def predict_click(self, **kwargs):
return None
def get_capabilities(self):
return ["step"]
class TestToolResolution:
"""Tests for ComputerAgent._resolve_tools()"""
@pytest.fixture
def mock_computer(self):
"""Create a mock Computer object"""
computer = Mock()
computer.interface = Mock()
computer.interface.interface = Mock() # For hotkey, etc.
computer.interface.playwright_exec = Mock()
return computer
@pytest.fixture
def mock_browser_tool(self):
"""Create a mock BrowserTool"""
tool = Mock(spec=BrowserTool)
return tool
@pytest.mark.asyncio
async def test_fara_auto_wraps_computer_to_browser_tool(self, mock_computer):
"""FARA auto-wraps Computer to BrowserTool with warning."""
from cua_agent.agent import ComputerAgent
# Patch find_agent_config to return FARA config
fara_config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r"(?i).*fara.*",
priority=0,
tool_type="browser",
)
# Patch is_agent_computer to recognize our mock as a computer
def mock_is_agent_computer(tool):
return tool is mock_computer
with patch("cua_agent.agent.find_agent_config", return_value=fara_config):
with patch("cua_agent.agent.is_agent_computer", side_effect=mock_is_agent_computer):
agent = ComputerAgent(model="cua/microsoft/fara-7b", tools=[mock_computer])
# Tool resolution happens in _resolve_tools, which is async
with pytest.warns(UserWarning, match="Auto-wrapping Computer to BrowserTool"):
resolved = await agent._resolve_tools(agent.tools, "browser")
# Should have wrapped to BrowserTool
assert len(resolved) == 1
assert isinstance(resolved[0], BrowserTool)
def test_fara_accepts_explicit_browser_tool_no_warning(self, mock_browser_tool):
"""FARA accepts explicit BrowserTool without warning."""
from cua_agent.agent import ComputerAgent
# Patch find_agent_config to return FARA config
fara_config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r"(?i).*fara.*",
priority=0,
tool_type="browser",
)
with patch("cua_agent.agent.find_agent_config", return_value=fara_config):
# Should not raise any warnings
with warnings.catch_warnings():
warnings.simplefilter("error")
agent = ComputerAgent(model="cua/microsoft/fara-7b", tools=[mock_browser_tool])
# Should keep the original BrowserTool
assert len(agent.tools) == 1
assert agent.tools[0] is mock_browser_tool
def test_claude_accepts_any_tool_no_wrapping(self, mock_computer):
"""Claude (general model) accepts any tool without wrapping."""
from cua_agent.agent import ComputerAgent
# Patch find_agent_config to return Claude config (no tool_type)
claude_config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r".*claude.*",
priority=0,
tool_type=None, # General model, no tool type requirement
)
with patch("cua_agent.agent.find_agent_config", return_value=claude_config):
with warnings.catch_warnings():
warnings.simplefilter("error") # Fail if any warning
agent = ComputerAgent(model="claude-sonnet-4", tools=[mock_computer])
# Should keep original Computer, not wrapped
assert len(agent.tools) == 1
assert agent.tools[0] is mock_computer
@pytest.mark.asyncio
async def test_custom_tools_pass_through(self, mock_computer):
"""Custom function tools pass through unchanged."""
from cua_agent.agent import ComputerAgent
def custom_tool():
"""A custom tool function."""
pass
# Patch find_agent_config to return FARA config
fara_config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r"(?i).*fara.*",
priority=0,
tool_type="browser",
)
# Patch is_agent_computer to recognize our mock as a computer
def mock_is_agent_computer(tool):
return tool is mock_computer
with patch("cua_agent.agent.find_agent_config", return_value=fara_config):
with patch("cua_agent.agent.is_agent_computer", side_effect=mock_is_agent_computer):
agent = ComputerAgent(
model="cua/microsoft/fara-7b", tools=[mock_computer, custom_tool]
)
# Tool resolution happens in _resolve_tools, which is async
with warnings.catch_warnings():
warnings.simplefilter("ignore")
resolved = await agent._resolve_tools(agent.tools, "browser")
# Should have wrapped computer but kept custom tool unchanged
assert len(resolved) == 2
assert isinstance(resolved[0], BrowserTool)
assert resolved[1] is custom_tool
class TestAgentConfigInfo:
"""Tests for AgentConfigInfo with tool_type field"""
def test_agent_config_info_with_tool_type(self):
"""AgentConfigInfo accepts tool_type parameter"""
config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r".*fara.*",
priority=0,
tool_type="browser",
)
assert config.tool_type == "browser"
def test_agent_config_info_without_tool_type(self):
"""AgentConfigInfo defaults tool_type to None"""
config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r".*claude.*",
priority=0,
)
assert config.tool_type is None
def test_agent_config_info_matches_model(self):
"""AgentConfigInfo.matches_model works correctly"""
config = AgentConfigInfo(
agent_class=MockAgentConfig,
models_regex=r"(?i).*fara-7b.*",
priority=0,
tool_type="browser",
)
assert config.matches_model("cua/microsoft/fara-7b")
assert config.matches_model("FARA-7B")
assert not config.matches_model("claude-sonnet-4")
class TestRegisterAgentDecorator:
"""Tests for @register_agent decorator with tool_type"""
def test_register_agent_with_tool_type(self):
"""@register_agent accepts tool_type parameter"""
from cua_agent.decorators import _agent_configs, register_agent
# Clear registry for test
original_configs = _agent_configs.copy()
_agent_configs.clear()
try:
@register_agent(models=r"test-model.*", tool_type="browser")
class TestAgentConfig:
async def predict_step(self, **kwargs):
pass
async def predict_click(self, **kwargs):
pass
def get_capabilities(self):
return ["step"]
# Find the registered config
from cua_agent.decorators import find_agent_config
config = find_agent_config("test-model-123")
assert config is not None
assert config.tool_type == "browser"
finally:
# Restore original registry
_agent_configs.clear()
_agent_configs.extend(original_configs)
def test_register_agent_without_tool_type(self):
"""@register_agent without tool_type defaults to None"""
from cua_agent.decorators import _agent_configs, register_agent
# Clear registry for test
original_configs = _agent_configs.copy()
_agent_configs.clear()
try:
@register_agent(models=r"general-model.*")
class GeneralAgentConfig:
async def predict_step(self, **kwargs):
pass
async def predict_click(self, **kwargs):
pass
def get_capabilities(self):
return ["step"]
# Find the registered config
from cua_agent.decorators import find_agent_config
config = find_agent_config("general-model-123")
assert config is not None
assert config.tool_type is None
finally:
# Restore original registry
_agent_configs.clear()
_agent_configs.extend(original_configs)