hud-python 0.3.5__py3-none-any.whl → 0.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -89
- hud/agents/__init__.py +15 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +370 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +379 -0
- hud/clients/fastmcp.py +222 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.1.dist-info/METADATA +476 -0
- hud_python-0.4.1.dist-info/RECORD +132 -0
- hud_python-0.4.1.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Extended tests for bash tool to improve coverage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from hud.tools.bash import ToolError, _BashSession
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestBashSessionExtended:
|
|
14
|
+
"""Extended tests for _BashSession to improve coverage."""
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_session_start_already_started(self):
|
|
18
|
+
"""Test starting a session that's already started."""
|
|
19
|
+
session = _BashSession()
|
|
20
|
+
session._started = True
|
|
21
|
+
|
|
22
|
+
with patch("asyncio.sleep") as mock_sleep:
|
|
23
|
+
mock_sleep.return_value = None
|
|
24
|
+
await session.start()
|
|
25
|
+
|
|
26
|
+
# Should call sleep and return early
|
|
27
|
+
mock_sleep.assert_called_once_with(0)
|
|
28
|
+
|
|
29
|
+
@pytest.mark.asyncio
|
|
30
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Unix-specific test")
|
|
31
|
+
async def test_session_start_unix_preexec(self):
|
|
32
|
+
"""Test session start on Unix systems uses preexec_fn."""
|
|
33
|
+
session = _BashSession()
|
|
34
|
+
|
|
35
|
+
with patch("asyncio.create_subprocess_shell") as mock_create:
|
|
36
|
+
mock_process = MagicMock()
|
|
37
|
+
mock_create.return_value = mock_process
|
|
38
|
+
|
|
39
|
+
await session.start()
|
|
40
|
+
|
|
41
|
+
# Check that preexec_fn was passed
|
|
42
|
+
call_kwargs = mock_create.call_args[1]
|
|
43
|
+
assert "preexec_fn" in call_kwargs
|
|
44
|
+
assert call_kwargs["preexec_fn"] is not None
|
|
45
|
+
|
|
46
|
+
def test_session_stop_with_terminated_process(self):
|
|
47
|
+
"""Test stopping a session with already terminated process."""
|
|
48
|
+
session = _BashSession()
|
|
49
|
+
session._started = True
|
|
50
|
+
|
|
51
|
+
# Mock process that's already terminated
|
|
52
|
+
mock_process = MagicMock()
|
|
53
|
+
mock_process.returncode = 0 # Process already exited
|
|
54
|
+
session._process = mock_process
|
|
55
|
+
|
|
56
|
+
# Should not raise error and not call terminate
|
|
57
|
+
session.stop()
|
|
58
|
+
mock_process.terminate.assert_not_called()
|
|
59
|
+
|
|
60
|
+
def test_session_stop_with_running_process(self):
|
|
61
|
+
"""Test stopping a session with running process."""
|
|
62
|
+
session = _BashSession()
|
|
63
|
+
session._started = True
|
|
64
|
+
|
|
65
|
+
# Mock process that's still running
|
|
66
|
+
mock_process = MagicMock()
|
|
67
|
+
mock_process.returncode = None
|
|
68
|
+
session._process = mock_process
|
|
69
|
+
|
|
70
|
+
session.stop()
|
|
71
|
+
mock_process.terminate.assert_called_once()
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_session_run_with_exited_process(self):
|
|
75
|
+
"""Test running command when process has already exited."""
|
|
76
|
+
session = _BashSession()
|
|
77
|
+
session._started = True
|
|
78
|
+
|
|
79
|
+
# Mock process that has exited
|
|
80
|
+
mock_process = MagicMock()
|
|
81
|
+
mock_process.returncode = 1
|
|
82
|
+
session._process = mock_process
|
|
83
|
+
|
|
84
|
+
with patch("asyncio.sleep") as mock_sleep:
|
|
85
|
+
mock_sleep.return_value = None
|
|
86
|
+
result = await session.run("echo test")
|
|
87
|
+
|
|
88
|
+
assert result.system == "tool must be restarted"
|
|
89
|
+
assert result.error == "bash has exited with returncode 1"
|
|
90
|
+
mock_sleep.assert_called_once_with(0)
|
|
91
|
+
|
|
92
|
+
@pytest.mark.asyncio
|
|
93
|
+
async def test_session_run_with_stderr_output(self):
|
|
94
|
+
"""Test command execution with stderr output."""
|
|
95
|
+
session = _BashSession()
|
|
96
|
+
session._started = True
|
|
97
|
+
|
|
98
|
+
# Mock process
|
|
99
|
+
mock_process = MagicMock()
|
|
100
|
+
mock_process.returncode = None
|
|
101
|
+
mock_process.stdin = MagicMock()
|
|
102
|
+
mock_process.stdin.write = MagicMock()
|
|
103
|
+
mock_process.stdin.drain = AsyncMock()
|
|
104
|
+
mock_process.stdout = MagicMock()
|
|
105
|
+
mock_process.stdout.readuntil = AsyncMock(return_value=b"stdout output\n<<exit>>\n")
|
|
106
|
+
mock_process.stderr = MagicMock()
|
|
107
|
+
mock_process.stderr.read = AsyncMock(return_value=b"stderr output\n")
|
|
108
|
+
|
|
109
|
+
session._process = mock_process
|
|
110
|
+
|
|
111
|
+
result = await session.run("command")
|
|
112
|
+
|
|
113
|
+
assert result.output == "stdout output\n"
|
|
114
|
+
assert result.error == "stderr output" # .strip() is called on stderr
|
|
115
|
+
|
|
116
|
+
@pytest.mark.asyncio
|
|
117
|
+
async def test_session_run_with_asyncio_timeout(self):
|
|
118
|
+
"""Test command execution timing out."""
|
|
119
|
+
session = _BashSession()
|
|
120
|
+
session._started = True
|
|
121
|
+
|
|
122
|
+
# Mock process
|
|
123
|
+
mock_process = MagicMock()
|
|
124
|
+
mock_process.returncode = None
|
|
125
|
+
mock_process.stdin = MagicMock()
|
|
126
|
+
mock_process.stdin.write = MagicMock()
|
|
127
|
+
mock_process.stdin.drain = AsyncMock()
|
|
128
|
+
mock_process.stdout = MagicMock()
|
|
129
|
+
# Simulate timeout
|
|
130
|
+
mock_process.stdout.readuntil = AsyncMock(side_effect=TimeoutError())
|
|
131
|
+
|
|
132
|
+
session._process = mock_process
|
|
133
|
+
|
|
134
|
+
# Should raise ToolError on timeout
|
|
135
|
+
with pytest.raises(ToolError) as exc_info:
|
|
136
|
+
await session.run("slow command")
|
|
137
|
+
|
|
138
|
+
assert "timed out" in str(exc_info.value)
|
|
139
|
+
assert "120.0 seconds" in str(exc_info.value)
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_session_run_with_stdout_exception(self):
|
|
143
|
+
"""Test command execution with exception reading stdout."""
|
|
144
|
+
session = _BashSession()
|
|
145
|
+
session._started = True
|
|
146
|
+
|
|
147
|
+
# Mock process
|
|
148
|
+
mock_process = MagicMock()
|
|
149
|
+
mock_process.returncode = None
|
|
150
|
+
mock_process.stdin = MagicMock()
|
|
151
|
+
mock_process.stdin.write = MagicMock()
|
|
152
|
+
mock_process.stdin.drain = AsyncMock()
|
|
153
|
+
mock_process.stdout = MagicMock()
|
|
154
|
+
# Simulate other exception
|
|
155
|
+
mock_process.stdout.readuntil = AsyncMock(side_effect=Exception("Read error"))
|
|
156
|
+
|
|
157
|
+
session._process = mock_process
|
|
158
|
+
|
|
159
|
+
# The exception should bubble up
|
|
160
|
+
with pytest.raises(Exception) as exc_info:
|
|
161
|
+
await session.run("bad command")
|
|
162
|
+
|
|
163
|
+
assert "Read error" in str(exc_info.value)
|
|
164
|
+
|
|
165
|
+
@pytest.mark.asyncio
|
|
166
|
+
async def test_session_run_with_stderr_exception(self):
|
|
167
|
+
"""Test command execution with exception reading stderr."""
|
|
168
|
+
session = _BashSession()
|
|
169
|
+
session._started = True
|
|
170
|
+
|
|
171
|
+
# Mock process
|
|
172
|
+
mock_process = MagicMock()
|
|
173
|
+
mock_process.returncode = None
|
|
174
|
+
mock_process.stdin = MagicMock()
|
|
175
|
+
mock_process.stdin.write = MagicMock()
|
|
176
|
+
mock_process.stdin.drain = AsyncMock()
|
|
177
|
+
mock_process.stdout = MagicMock()
|
|
178
|
+
mock_process.stdout.readuntil = AsyncMock(return_value=b"output\n<<exit>>\n")
|
|
179
|
+
mock_process.stderr = MagicMock()
|
|
180
|
+
# Simulate stderr read error
|
|
181
|
+
mock_process.stderr.read = AsyncMock(side_effect=Exception("Stderr read error"))
|
|
182
|
+
|
|
183
|
+
session._process = mock_process
|
|
184
|
+
|
|
185
|
+
# stderr exceptions should also bubble up
|
|
186
|
+
with pytest.raises(Exception) as exc_info:
|
|
187
|
+
await session.run("command")
|
|
188
|
+
|
|
189
|
+
assert "Stderr read error" in str(exc_info.value)
|
|
190
|
+
|
|
191
|
+
def test_bash_session_different_shells(self):
|
|
192
|
+
"""Test that different shells are used on different platforms."""
|
|
193
|
+
session = _BashSession()
|
|
194
|
+
|
|
195
|
+
# Currently, _BashSession always uses /bin/bash regardless of platform
|
|
196
|
+
# This test should verify the actual implementation
|
|
197
|
+
assert session.command == "/bin/bash"
|
hud/tools/tests/test_computer.py
CHANGED
|
@@ -1,52 +1,425 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
from hud.tools.computer.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from mcp.types import ImageContent, TextContent
|
|
7
|
+
|
|
8
|
+
from hud.tools.computer.anthropic import AnthropicComputerTool
|
|
9
|
+
from hud.tools.computer.hud import HudComputerTool
|
|
10
|
+
from hud.tools.computer.openai import OpenAIComputerTool
|
|
11
|
+
from hud.tools.executors.base import BaseExecutor
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.asyncio
|
|
15
|
+
async def test_hud_computer_screenshot():
|
|
16
|
+
comp = HudComputerTool()
|
|
17
|
+
blocks = await comp(action="screenshot")
|
|
18
|
+
# Screenshot might return ImageContent or TextContent (if error)
|
|
19
|
+
assert blocks is not None
|
|
20
|
+
assert len(blocks) > 0
|
|
21
|
+
assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
25
|
+
async def test_hud_computer_click_simulation():
|
|
26
|
+
comp = HudComputerTool()
|
|
27
|
+
blocks = await comp(action="click", x=10, y=10)
|
|
28
|
+
# Should return text confirming execution or screenshot block
|
|
29
|
+
assert blocks
|
|
30
|
+
assert len(blocks) > 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_openai_computer_screenshot():
|
|
35
|
+
comp = OpenAIComputerTool()
|
|
36
|
+
blocks = await comp(type="screenshot")
|
|
37
|
+
assert blocks is not None
|
|
38
|
+
assert len(blocks) > 0
|
|
39
|
+
assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.asyncio
|
|
43
|
+
async def test_anthropic_computer_screenshot():
|
|
44
|
+
comp = AnthropicComputerTool()
|
|
45
|
+
blocks = await comp(action="screenshot")
|
|
46
|
+
assert blocks is not None
|
|
47
|
+
assert len(blocks) > 0
|
|
48
|
+
assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_openai_computer_click():
|
|
53
|
+
comp = OpenAIComputerTool()
|
|
54
|
+
blocks = await comp(type="click", x=5, y=5)
|
|
55
|
+
assert blocks
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestHudComputerToolExtended:
|
|
59
|
+
"""Extended tests for HudComputerTool covering edge cases and platform logic."""
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def base_executor(self):
|
|
63
|
+
"""Create a BaseExecutor instance for testing."""
|
|
64
|
+
return BaseExecutor()
|
|
65
|
+
|
|
66
|
+
@pytest.mark.asyncio
|
|
67
|
+
async def test_explicit_base_executor(self, base_executor):
|
|
68
|
+
"""Test explicitly using BaseExecutor."""
|
|
69
|
+
tool = HudComputerTool(executor=base_executor)
|
|
70
|
+
assert tool.executor is base_executor
|
|
71
|
+
|
|
72
|
+
# Test that actions work with base executor
|
|
73
|
+
result = await tool(action="click", x=100, y=200)
|
|
74
|
+
assert result
|
|
75
|
+
assert any(
|
|
76
|
+
"[SIMULATED]" in content.text for content in result if isinstance(content, TextContent)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_platform_auto_selection_linux(self):
|
|
81
|
+
"""Test platform auto-selection on Linux."""
|
|
82
|
+
with (
|
|
83
|
+
patch("platform.system", return_value="Linux"),
|
|
84
|
+
patch("hud.tools.executors.xdo.XDOExecutor.is_available", return_value=False),
|
|
85
|
+
patch(
|
|
86
|
+
"hud.tools.executors.pyautogui.PyAutoGUIExecutor.is_available",
|
|
87
|
+
return_value=False,
|
|
88
|
+
),
|
|
89
|
+
):
|
|
90
|
+
tool = HudComputerTool()
|
|
91
|
+
assert isinstance(tool.executor, BaseExecutor)
|
|
92
|
+
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_platform_auto_selection_windows(self):
|
|
95
|
+
"""Test platform auto-selection on Windows."""
|
|
96
|
+
with (
|
|
97
|
+
patch("platform.system", return_value="Windows"),
|
|
98
|
+
patch(
|
|
99
|
+
"hud.tools.executors.pyautogui.PyAutoGUIExecutor.is_available", return_value=False
|
|
100
|
+
),
|
|
101
|
+
):
|
|
102
|
+
tool = HudComputerTool()
|
|
103
|
+
assert isinstance(tool.executor, BaseExecutor)
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_platform_xdo_fallback(self):
|
|
107
|
+
"""Test XDO platform fallback to BaseExecutor."""
|
|
108
|
+
with patch("hud.tools.executors.xdo.XDOExecutor.is_available", return_value=False):
|
|
109
|
+
tool = HudComputerTool(platform_type="xdo")
|
|
110
|
+
assert isinstance(tool.executor, BaseExecutor)
|
|
111
|
+
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_platform_pyautogui_fallback(self):
|
|
114
|
+
"""Test PyAutoGUI platform fallback to BaseExecutor."""
|
|
115
|
+
with patch(
|
|
116
|
+
"hud.tools.executors.pyautogui.PyAutoGUIExecutor.is_available", return_value=False
|
|
117
|
+
):
|
|
118
|
+
tool = HudComputerTool(platform_type="pyautogui")
|
|
119
|
+
assert isinstance(tool.executor, BaseExecutor)
|
|
120
|
+
|
|
121
|
+
@pytest.mark.asyncio
|
|
122
|
+
async def test_invalid_platform_type(self):
|
|
123
|
+
"""Test invalid platform type raises ValueError."""
|
|
124
|
+
with pytest.raises(ValueError, match="Invalid platform_type"):
|
|
125
|
+
HudComputerTool(platform_type="invalid_platform") # type: ignore[arg-type]
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_coordinate_scaling(self, base_executor):
|
|
129
|
+
"""Test coordinate scaling with different screen sizes."""
|
|
130
|
+
# Test with custom dimensions that require scaling
|
|
131
|
+
tool = HudComputerTool(executor=base_executor, width=800, height=600)
|
|
132
|
+
|
|
133
|
+
# Test click with scaling
|
|
134
|
+
result = await tool(action="click", x=400, y=300)
|
|
135
|
+
assert result
|
|
136
|
+
|
|
137
|
+
# Test that coordinates are scaled properly
|
|
138
|
+
assert tool.scale_x == 800 / 1920 # Default environment width is 1920
|
|
139
|
+
assert tool.scale_y == 600 / 1080 # Default environment height is 1080
|
|
140
|
+
assert tool.needs_scaling is True
|
|
141
|
+
|
|
142
|
+
@pytest.mark.asyncio
|
|
143
|
+
async def test_no_scaling_needed(self, base_executor):
|
|
144
|
+
"""Test when no scaling is needed."""
|
|
145
|
+
tool = HudComputerTool(executor=base_executor, width=1920, height=1080)
|
|
146
|
+
assert tool.needs_scaling is False
|
|
147
|
+
assert tool.scale_x == 1.0
|
|
148
|
+
assert tool.scale_y == 1.0
|
|
149
|
+
|
|
150
|
+
@pytest.mark.asyncio
|
|
151
|
+
async def test_type_action(self, base_executor):
|
|
152
|
+
"""Test type action with BaseExecutor."""
|
|
153
|
+
tool = HudComputerTool(executor=base_executor)
|
|
154
|
+
result = await tool(action="type", text="Hello World", enter_after=True)
|
|
155
|
+
assert result
|
|
156
|
+
assert any(
|
|
157
|
+
"[SIMULATED] Type" in content.text
|
|
158
|
+
for content in result
|
|
159
|
+
if isinstance(content, TextContent)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_press_action(self, base_executor):
|
|
164
|
+
"""Test press action with BaseExecutor."""
|
|
165
|
+
tool = HudComputerTool(executor=base_executor)
|
|
166
|
+
result = await tool(action="press", keys=["ctrl", "c"])
|
|
167
|
+
assert result
|
|
168
|
+
assert any(
|
|
169
|
+
"[SIMULATED] Press" in content.text
|
|
170
|
+
for content in result
|
|
171
|
+
if isinstance(content, TextContent)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@pytest.mark.asyncio
|
|
175
|
+
async def test_scroll_action(self, base_executor):
|
|
176
|
+
"""Test scroll action with BaseExecutor."""
|
|
177
|
+
tool = HudComputerTool(executor=base_executor)
|
|
178
|
+
result = await tool(action="scroll", x=500, y=500, scroll_x=0, scroll_y=5)
|
|
179
|
+
assert result
|
|
180
|
+
assert any(
|
|
181
|
+
"Scroll" in content.text for content in result if isinstance(content, TextContent)
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
@pytest.mark.asyncio
|
|
185
|
+
async def test_move_action(self, base_executor):
|
|
186
|
+
"""Test move action with BaseExecutor."""
|
|
187
|
+
tool = HudComputerTool(executor=base_executor)
|
|
188
|
+
result = await tool(action="move", x=100, y=100)
|
|
189
|
+
assert result
|
|
190
|
+
assert any("Move" in content.text for content in result if isinstance(content, TextContent))
|
|
191
|
+
|
|
192
|
+
@pytest.mark.asyncio
|
|
193
|
+
async def test_drag_action(self, base_executor):
|
|
194
|
+
"""Test drag action with BaseExecutor."""
|
|
195
|
+
tool = HudComputerTool(executor=base_executor)
|
|
196
|
+
result = await tool(action="drag", path=[(100, 100), (200, 200)])
|
|
197
|
+
assert result
|
|
198
|
+
assert any("Drag" in content.text for content in result if isinstance(content, TextContent))
|
|
199
|
+
|
|
200
|
+
@pytest.mark.asyncio
|
|
201
|
+
async def test_wait_action(self, base_executor):
|
|
202
|
+
"""Test wait action with BaseExecutor."""
|
|
203
|
+
tool = HudComputerTool(executor=base_executor)
|
|
204
|
+
result = await tool(action="wait", time=100) # 100ms for quick test
|
|
205
|
+
assert result
|
|
206
|
+
assert any("Wait" in content.text for content in result if isinstance(content, TextContent))
|
|
207
|
+
|
|
208
|
+
@pytest.mark.asyncio
|
|
209
|
+
async def test_keydown_keyup_actions(self, base_executor):
|
|
210
|
+
"""Test keydown and keyup actions with BaseExecutor."""
|
|
211
|
+
tool = HudComputerTool(executor=base_executor)
|
|
212
|
+
|
|
213
|
+
# Test keydown
|
|
214
|
+
result = await tool(action="keydown", keys=["shift"])
|
|
215
|
+
assert result
|
|
216
|
+
|
|
217
|
+
# Test keyup
|
|
218
|
+
result = await tool(action="keyup", keys=["shift"])
|
|
219
|
+
assert result
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_hold_key_action(self, base_executor):
|
|
223
|
+
"""Test hold_key action with BaseExecutor."""
|
|
224
|
+
tool = HudComputerTool(executor=base_executor)
|
|
225
|
+
result = await tool(action="hold_key", text="a", duration=0.1)
|
|
226
|
+
assert result
|
|
227
|
+
|
|
228
|
+
@pytest.mark.asyncio
|
|
229
|
+
async def test_mouse_down_up_actions(self, base_executor):
|
|
230
|
+
"""Test mouse_down and mouse_up actions with BaseExecutor."""
|
|
231
|
+
tool = HudComputerTool(executor=base_executor)
|
|
232
|
+
|
|
233
|
+
# Test mouse_down
|
|
234
|
+
result = await tool(action="mouse_down", button="left")
|
|
235
|
+
assert result
|
|
236
|
+
|
|
237
|
+
# Test mouse_up
|
|
238
|
+
result = await tool(action="mouse_up", button="left")
|
|
239
|
+
assert result
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_position_action(self, base_executor):
|
|
243
|
+
"""Test position action with BaseExecutor."""
|
|
244
|
+
tool = HudComputerTool(executor=base_executor)
|
|
245
|
+
result = await tool(action="position")
|
|
246
|
+
assert result
|
|
247
|
+
|
|
248
|
+
@pytest.mark.asyncio
|
|
249
|
+
async def test_response_action(self, base_executor):
|
|
250
|
+
"""Test response action."""
|
|
251
|
+
tool = HudComputerTool(executor=base_executor)
|
|
252
|
+
result = await tool(action="response", text="Test response")
|
|
253
|
+
assert result
|
|
254
|
+
assert len(result) == 1
|
|
255
|
+
assert isinstance(result[0], TextContent)
|
|
256
|
+
assert result[0].text == "Test response"
|
|
257
|
+
|
|
258
|
+
@pytest.mark.asyncio
|
|
259
|
+
async def test_click_with_different_buttons(self, base_executor):
|
|
260
|
+
"""Test click with different mouse buttons."""
|
|
261
|
+
tool = HudComputerTool(executor=base_executor)
|
|
262
|
+
|
|
263
|
+
# Right click
|
|
264
|
+
result = await tool(action="click", x=100, y=100, button="right")
|
|
265
|
+
assert result
|
|
266
|
+
|
|
267
|
+
# Middle click
|
|
268
|
+
result = await tool(action="click", x=100, y=100, button="middle")
|
|
269
|
+
assert result
|
|
270
|
+
|
|
271
|
+
# Double click (using pattern)
|
|
272
|
+
result = await tool(action="click", x=100, y=100, pattern=[100])
|
|
273
|
+
assert result
|
|
274
|
+
|
|
275
|
+
@pytest.mark.asyncio
|
|
276
|
+
async def test_invalid_action(self, base_executor):
|
|
277
|
+
"""Test invalid action returns error."""
|
|
278
|
+
tool = HudComputerTool(executor=base_executor)
|
|
279
|
+
|
|
280
|
+
with pytest.raises(Exception): # Will raise McpError
|
|
281
|
+
await tool(action="invalid_action")
|
|
282
|
+
|
|
283
|
+
@pytest.mark.asyncio
|
|
284
|
+
async def test_screenshot_action(self, base_executor):
|
|
285
|
+
"""Test screenshot action."""
|
|
286
|
+
tool = HudComputerTool(executor=base_executor)
|
|
287
|
+
|
|
288
|
+
# Mock the screenshot method
|
|
289
|
+
base_executor.screenshot = AsyncMock(return_value="fake_base64_data")
|
|
290
|
+
|
|
291
|
+
result = await tool(action="screenshot")
|
|
292
|
+
assert result
|
|
293
|
+
assert any(isinstance(content, ImageContent) for content in result)
|
|
294
|
+
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_screenshot_rescaling(self, base_executor):
|
|
297
|
+
"""Test screenshot rescaling functionality."""
|
|
298
|
+
tool = HudComputerTool(executor=base_executor, width=800, height=600, rescale_images=True)
|
|
299
|
+
|
|
300
|
+
# Mock the screenshot method
|
|
301
|
+
base_executor.screenshot = AsyncMock(return_value="fake_base64_data")
|
|
302
|
+
|
|
303
|
+
# Mock the rescale method
|
|
304
|
+
tool._rescale_screenshot = AsyncMock(return_value="rescaled_base64_data")
|
|
305
|
+
|
|
306
|
+
result = await tool(action="screenshot")
|
|
307
|
+
assert result
|
|
308
|
+
# The rescale method is called twice - once for the screenshot action,
|
|
309
|
+
# and once when processing the result
|
|
310
|
+
assert tool._rescale_screenshot.call_count == 2
|
|
311
|
+
tool._rescale_screenshot.assert_any_call("fake_base64_data")
|
|
312
|
+
|
|
313
|
+
@pytest.mark.asyncio
|
|
314
|
+
async def test_executor_initialization_with_display_num(self):
|
|
315
|
+
"""Test executor initialization with display number."""
|
|
316
|
+
with patch(
|
|
317
|
+
"hud.tools.executors.pyautogui.PyAutoGUIExecutor.is_available", return_value=False
|
|
318
|
+
):
|
|
319
|
+
tool = HudComputerTool(display_num=1)
|
|
320
|
+
assert tool.display_num == 1
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_coordinate_none_values(self, base_executor):
|
|
324
|
+
"""Test actions with None coordinate values."""
|
|
325
|
+
tool = HudComputerTool(executor=base_executor)
|
|
326
|
+
|
|
327
|
+
# Test press without coordinates (keyboard shortcut)
|
|
328
|
+
result = await tool(action="press", keys=["ctrl", "a"])
|
|
329
|
+
assert result
|
|
330
|
+
|
|
331
|
+
# Test type without coordinates
|
|
332
|
+
result = await tool(action="type", text="test")
|
|
333
|
+
assert result
|
|
334
|
+
|
|
335
|
+
@pytest.mark.asyncio
|
|
336
|
+
async def test_tool_metadata(self, base_executor):
|
|
337
|
+
"""Test tool metadata is set correctly."""
|
|
338
|
+
tool = HudComputerTool(
|
|
339
|
+
executor=base_executor,
|
|
340
|
+
name="custom_computer",
|
|
341
|
+
title="Custom Computer Tool",
|
|
342
|
+
description="Custom description",
|
|
343
|
+
)
|
|
344
|
+
assert tool.name == "custom_computer"
|
|
345
|
+
assert tool.title == "Custom Computer Tool"
|
|
346
|
+
assert tool.description == "Custom description"
|
|
347
|
+
|
|
348
|
+
# Test defaults
|
|
349
|
+
default_tool = HudComputerTool(executor=base_executor)
|
|
350
|
+
assert default_tool.name == "computer"
|
|
351
|
+
assert default_tool.title == "Computer Control"
|
|
352
|
+
assert default_tool.description == "Control computer with mouse, keyboard, and screenshots"
|
|
353
|
+
|
|
354
|
+
@pytest.mark.asyncio
|
|
355
|
+
async def test_missing_required_parameters(self, base_executor):
|
|
356
|
+
"""Test actions that are missing required parameters."""
|
|
357
|
+
tool = HudComputerTool(executor=base_executor)
|
|
358
|
+
|
|
359
|
+
# Test type without text
|
|
360
|
+
from hud.tools.types import ToolError
|
|
361
|
+
|
|
362
|
+
with pytest.raises(ToolError, match="text parameter is required"):
|
|
363
|
+
await tool(action="type", text=None)
|
|
364
|
+
|
|
365
|
+
# Test press without keys
|
|
366
|
+
with pytest.raises(ToolError, match="keys parameter is required"):
|
|
367
|
+
await tool(action="press", keys=None)
|
|
368
|
+
|
|
369
|
+
# Test wait without time
|
|
370
|
+
with pytest.raises(ToolError, match="time parameter is required"):
|
|
371
|
+
await tool(action="wait", time=None)
|
|
372
|
+
|
|
373
|
+
# Test drag without path
|
|
374
|
+
with pytest.raises(ToolError, match="path parameter is required"):
|
|
375
|
+
await tool(action="drag", path=None)
|
|
376
|
+
|
|
377
|
+
@pytest.mark.asyncio
|
|
378
|
+
async def test_relative_move(self, base_executor):
|
|
379
|
+
"""Test relative move with offsets."""
|
|
380
|
+
tool = HudComputerTool(executor=base_executor)
|
|
381
|
+
result = await tool(action="move", offset_x=50, offset_y=50)
|
|
382
|
+
assert result
|
|
383
|
+
|
|
384
|
+
@pytest.mark.asyncio
|
|
385
|
+
async def test_screenshot_failure(self, base_executor):
|
|
386
|
+
"""Test screenshot failure handling."""
|
|
387
|
+
tool = HudComputerTool(executor=base_executor)
|
|
388
|
+
|
|
389
|
+
# Mock screenshot to return None (failure)
|
|
390
|
+
base_executor.screenshot = AsyncMock(return_value=None)
|
|
391
|
+
|
|
392
|
+
result = await tool(action="screenshot")
|
|
393
|
+
assert result
|
|
394
|
+
# Should contain error message
|
|
395
|
+
assert any(
|
|
396
|
+
"Failed" in content.text for content in result if isinstance(content, TextContent)
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@pytest.mark.asyncio
|
|
400
|
+
async def test_platform_selection_with_available_executors(self):
|
|
401
|
+
"""Test platform selection when executors are available."""
|
|
402
|
+
# Test Linux with XDO available
|
|
403
|
+
mock_xdo_instance = MagicMock()
|
|
404
|
+
with (
|
|
405
|
+
patch("platform.system", return_value="Linux"),
|
|
406
|
+
patch("hud.tools.executors.xdo.XDOExecutor.is_available", return_value=True),
|
|
407
|
+
patch("hud.tools.computer.hud.XDOExecutor", return_value=mock_xdo_instance) as mock_xdo,
|
|
408
|
+
):
|
|
409
|
+
tool = HudComputerTool(platform_type="auto")
|
|
410
|
+
mock_xdo.assert_called_once()
|
|
411
|
+
assert tool.executor is mock_xdo_instance
|
|
412
|
+
|
|
413
|
+
# Test with PyAutoGUI available
|
|
414
|
+
mock_pyautogui_instance = MagicMock()
|
|
415
|
+
with (
|
|
416
|
+
patch(
|
|
417
|
+
"hud.tools.executors.pyautogui.PyAutoGUIExecutor.is_available", return_value=True
|
|
418
|
+
),
|
|
419
|
+
patch(
|
|
420
|
+
"hud.tools.computer.hud.PyAutoGUIExecutor", return_value=mock_pyautogui_instance
|
|
421
|
+
) as mock_pyautogui,
|
|
422
|
+
):
|
|
423
|
+
tool = HudComputerTool(platform_type="pyautogui")
|
|
424
|
+
mock_pyautogui.assert_called_once()
|
|
425
|
+
assert tool.executor is mock_pyautogui_instance
|