hud-python 0.4.1__py3-none-any.whl → 0.4.3__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 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- 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 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
|
@@ -1,338 +1,338 @@
|
|
|
1
|
-
"""Tests for BaseExecutor."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from unittest.mock import patch
|
|
6
|
-
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
from hud.tools.executors.base import BaseExecutor
|
|
10
|
-
from hud.tools.types import ContentResult
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TestBaseExecutor:
|
|
14
|
-
"""Tests for BaseExecutor simulated actions."""
|
|
15
|
-
|
|
16
|
-
def test_init(self):
|
|
17
|
-
"""Test BaseExecutor initialization."""
|
|
18
|
-
# Without display num - defaults to computer_settings.DISPLAY_NUM
|
|
19
|
-
executor = BaseExecutor()
|
|
20
|
-
assert executor.display_num == 0 # Default from computer_settings
|
|
21
|
-
assert executor._screenshot_delay == 0.5
|
|
22
|
-
|
|
23
|
-
# With display num
|
|
24
|
-
executor = BaseExecutor(display_num=1)
|
|
25
|
-
assert executor.display_num == 1
|
|
26
|
-
|
|
27
|
-
@pytest.mark.asyncio
|
|
28
|
-
async def test_click_basic(self):
|
|
29
|
-
"""Test basic click action."""
|
|
30
|
-
executor = BaseExecutor()
|
|
31
|
-
result = await executor.click(x=100, y=200, button="left", take_screenshot=False)
|
|
32
|
-
|
|
33
|
-
assert isinstance(result, ContentResult)
|
|
34
|
-
assert result.output == "[SIMULATED] Click at (100, 200) with left button"
|
|
35
|
-
assert result.base64_image is None # No screenshot requested
|
|
36
|
-
|
|
37
|
-
@pytest.mark.asyncio
|
|
38
|
-
async def test_click_with_screenshot(self):
|
|
39
|
-
"""Test click with screenshot."""
|
|
40
|
-
executor = BaseExecutor()
|
|
41
|
-
result = await executor.click(x=100, y=200, take_screenshot=True)
|
|
42
|
-
|
|
43
|
-
assert isinstance(result, ContentResult)
|
|
44
|
-
assert result.output == "[SIMULATED] Click at (100, 200) with left button"
|
|
45
|
-
assert result.base64_image is not None # Screenshot included
|
|
46
|
-
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_click_with_pattern(self):
|
|
49
|
-
"""Test click with multi-click pattern."""
|
|
50
|
-
executor = BaseExecutor()
|
|
51
|
-
result = await executor.click(x=100, y=200, pattern=[100, 50], take_screenshot=False)
|
|
52
|
-
|
|
53
|
-
assert isinstance(result, ContentResult)
|
|
54
|
-
assert result.output is not None
|
|
55
|
-
assert (
|
|
56
|
-
"[SIMULATED] Click at (100, 200) with left button (multi-click pattern: [100, 50])"
|
|
57
|
-
in result.output
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
@pytest.mark.asyncio
|
|
61
|
-
async def test_click_with_hold_keys(self):
|
|
62
|
-
"""Test click while holding keys."""
|
|
63
|
-
executor = BaseExecutor()
|
|
64
|
-
result = await executor.click(
|
|
65
|
-
x=100, y=200, hold_keys=["ctrl", "shift"], take_screenshot=False
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
assert isinstance(result, ContentResult)
|
|
69
|
-
assert result.output is not None
|
|
70
|
-
assert "while holding ['ctrl', 'shift']" in result.output
|
|
71
|
-
|
|
72
|
-
@pytest.mark.asyncio
|
|
73
|
-
async def test_type_basic(self):
|
|
74
|
-
"""Test basic typing."""
|
|
75
|
-
executor = BaseExecutor()
|
|
76
|
-
result = await executor.write("Hello World", take_screenshot=False)
|
|
77
|
-
|
|
78
|
-
assert isinstance(result, ContentResult)
|
|
79
|
-
assert result.output == "[SIMULATED] Type 'Hello World'"
|
|
80
|
-
|
|
81
|
-
@pytest.mark.asyncio
|
|
82
|
-
async def test_type_with_enter(self):
|
|
83
|
-
"""Test typing with enter."""
|
|
84
|
-
executor = BaseExecutor()
|
|
85
|
-
result = await executor.write("Hello", enter_after=True, take_screenshot=False)
|
|
86
|
-
|
|
87
|
-
assert isinstance(result, ContentResult)
|
|
88
|
-
assert result.output == "[SIMULATED] Type 'Hello' followed by Enter"
|
|
89
|
-
|
|
90
|
-
@pytest.mark.asyncio
|
|
91
|
-
async def test_press_keys(self):
|
|
92
|
-
"""Test pressing key combination."""
|
|
93
|
-
executor = BaseExecutor()
|
|
94
|
-
result = await executor.press(["ctrl", "c"], take_screenshot=False)
|
|
95
|
-
|
|
96
|
-
assert isinstance(result, ContentResult)
|
|
97
|
-
assert result.output == "[SIMULATED] Press key combination: ctrl+c"
|
|
98
|
-
|
|
99
|
-
@pytest.mark.asyncio
|
|
100
|
-
async def test_key_single(self):
|
|
101
|
-
"""Test pressing single key."""
|
|
102
|
-
executor = BaseExecutor()
|
|
103
|
-
result = await executor.key("Return", take_screenshot=False)
|
|
104
|
-
|
|
105
|
-
assert isinstance(result, ContentResult)
|
|
106
|
-
assert result.output == "[SIMULATED] Press key: Return"
|
|
107
|
-
|
|
108
|
-
@pytest.mark.asyncio
|
|
109
|
-
async def test_keydown(self):
|
|
110
|
-
"""Test key down action."""
|
|
111
|
-
executor = BaseExecutor()
|
|
112
|
-
result = await executor.keydown(["shift", "ctrl"], take_screenshot=False)
|
|
113
|
-
|
|
114
|
-
assert isinstance(result, ContentResult)
|
|
115
|
-
assert result.output == "[SIMULATED] Key down: shift, ctrl"
|
|
116
|
-
|
|
117
|
-
@pytest.mark.asyncio
|
|
118
|
-
async def test_keyup(self):
|
|
119
|
-
"""Test key up action."""
|
|
120
|
-
executor = BaseExecutor()
|
|
121
|
-
result = await executor.keyup(["shift", "ctrl"], take_screenshot=False)
|
|
122
|
-
|
|
123
|
-
assert isinstance(result, ContentResult)
|
|
124
|
-
assert result.output == "[SIMULATED] Key up: shift, ctrl"
|
|
125
|
-
|
|
126
|
-
@pytest.mark.asyncio
|
|
127
|
-
async def test_scroll_basic(self):
|
|
128
|
-
"""Test basic scroll."""
|
|
129
|
-
executor = BaseExecutor()
|
|
130
|
-
result = await executor.scroll(x=100, y=200, scroll_y=5, take_screenshot=False)
|
|
131
|
-
|
|
132
|
-
assert isinstance(result, ContentResult)
|
|
133
|
-
assert result.output is not None
|
|
134
|
-
assert "[SIMULATED] Scroll at (100, 200)" in result.output
|
|
135
|
-
assert "vertically by 5" in result.output
|
|
136
|
-
|
|
137
|
-
@pytest.mark.asyncio
|
|
138
|
-
async def test_scroll_horizontal(self):
|
|
139
|
-
"""Test horizontal scroll."""
|
|
140
|
-
executor = BaseExecutor()
|
|
141
|
-
result = await executor.scroll(scroll_x=10, take_screenshot=False)
|
|
142
|
-
|
|
143
|
-
assert isinstance(result, ContentResult)
|
|
144
|
-
assert result.output is not None
|
|
145
|
-
assert "[SIMULATED] Scroll" in result.output
|
|
146
|
-
assert "horizontally by 10" in result.output
|
|
147
|
-
|
|
148
|
-
@pytest.mark.asyncio
|
|
149
|
-
async def test_scroll_with_hold_keys(self):
|
|
150
|
-
"""Test scroll with held keys."""
|
|
151
|
-
executor = BaseExecutor()
|
|
152
|
-
result = await executor.scroll(
|
|
153
|
-
x=100, y=200, scroll_y=5, hold_keys=["shift"], take_screenshot=False
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
assert isinstance(result, ContentResult)
|
|
157
|
-
assert result.output is not None
|
|
158
|
-
assert "while holding ['shift']" in result.output
|
|
159
|
-
|
|
160
|
-
@pytest.mark.asyncio
|
|
161
|
-
async def test_move_absolute(self):
|
|
162
|
-
"""Test absolute mouse movement."""
|
|
163
|
-
executor = BaseExecutor()
|
|
164
|
-
result = await executor.move(x=300, y=400, take_screenshot=False)
|
|
165
|
-
|
|
166
|
-
assert isinstance(result, ContentResult)
|
|
167
|
-
assert result.output == "[SIMULATED] Move mouse to (300, 400)"
|
|
168
|
-
|
|
169
|
-
@pytest.mark.asyncio
|
|
170
|
-
async def test_move_relative(self):
|
|
171
|
-
"""Test relative mouse movement."""
|
|
172
|
-
executor = BaseExecutor()
|
|
173
|
-
result = await executor.move(offset_x=50, offset_y=-30, take_screenshot=False)
|
|
174
|
-
|
|
175
|
-
assert isinstance(result, ContentResult)
|
|
176
|
-
assert result.output == "[SIMULATED] Move mouse by offset (50, -30)"
|
|
177
|
-
|
|
178
|
-
@pytest.mark.asyncio
|
|
179
|
-
async def test_move_no_coords(self):
|
|
180
|
-
"""Test move with no coordinates."""
|
|
181
|
-
executor = BaseExecutor()
|
|
182
|
-
result = await executor.move(take_screenshot=False)
|
|
183
|
-
|
|
184
|
-
assert isinstance(result, ContentResult)
|
|
185
|
-
assert result.output == "[SIMULATED] Move mouse (no coordinates specified)"
|
|
186
|
-
|
|
187
|
-
@pytest.mark.asyncio
|
|
188
|
-
async def test_drag_basic(self):
|
|
189
|
-
"""Test basic drag operation."""
|
|
190
|
-
executor = BaseExecutor()
|
|
191
|
-
path = [(100, 100), (200, 200)]
|
|
192
|
-
result = await executor.drag(path, take_screenshot=False)
|
|
193
|
-
|
|
194
|
-
assert isinstance(result, ContentResult)
|
|
195
|
-
assert result.output == "[SIMULATED] Drag from (100, 100) to (200, 200)"
|
|
196
|
-
|
|
197
|
-
@pytest.mark.asyncio
|
|
198
|
-
async def test_drag_with_intermediate_points(self):
|
|
199
|
-
"""Test drag with intermediate points."""
|
|
200
|
-
executor = BaseExecutor()
|
|
201
|
-
path = [(100, 100), (150, 150), (200, 200)]
|
|
202
|
-
result = await executor.drag(path, take_screenshot=False)
|
|
203
|
-
|
|
204
|
-
assert isinstance(result, ContentResult)
|
|
205
|
-
assert result.output is not None
|
|
206
|
-
assert (
|
|
207
|
-
"[SIMULATED] Drag from (100, 100) to (200, 200) via 1 intermediate points"
|
|
208
|
-
in result.output
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
@pytest.mark.asyncio
|
|
212
|
-
async def test_drag_invalid_path(self):
|
|
213
|
-
"""Test drag with invalid path."""
|
|
214
|
-
executor = BaseExecutor()
|
|
215
|
-
result = await executor.drag([(100, 100)], take_screenshot=False) # Only one point
|
|
216
|
-
|
|
217
|
-
assert isinstance(result, ContentResult)
|
|
218
|
-
assert result.error == "Drag path must have at least 2 points"
|
|
219
|
-
assert result.output is None
|
|
220
|
-
|
|
221
|
-
@pytest.mark.asyncio
|
|
222
|
-
async def test_drag_with_hold_keys(self):
|
|
223
|
-
"""Test drag with held keys."""
|
|
224
|
-
executor = BaseExecutor()
|
|
225
|
-
path = [(100, 100), (200, 200)]
|
|
226
|
-
result = await executor.drag(path, hold_keys=["alt"], take_screenshot=False)
|
|
227
|
-
|
|
228
|
-
assert isinstance(result, ContentResult)
|
|
229
|
-
assert result.output is not None
|
|
230
|
-
assert "while holding ['alt']" in result.output
|
|
231
|
-
|
|
232
|
-
@pytest.mark.asyncio
|
|
233
|
-
async def test_mouse_down(self):
|
|
234
|
-
"""Test mouse down action."""
|
|
235
|
-
executor = BaseExecutor()
|
|
236
|
-
result = await executor.mouse_down(button="right", take_screenshot=False)
|
|
237
|
-
|
|
238
|
-
assert isinstance(result, ContentResult)
|
|
239
|
-
assert result.output == "[SIMULATED] Mouse down: right button"
|
|
240
|
-
|
|
241
|
-
@pytest.mark.asyncio
|
|
242
|
-
async def test_mouse_up(self):
|
|
243
|
-
"""Test mouse up action."""
|
|
244
|
-
executor = BaseExecutor()
|
|
245
|
-
result = await executor.mouse_up(button="middle", take_screenshot=False)
|
|
246
|
-
|
|
247
|
-
assert isinstance(result, ContentResult)
|
|
248
|
-
assert result.output == "[SIMULATED] Mouse up: middle button"
|
|
249
|
-
|
|
250
|
-
@pytest.mark.asyncio
|
|
251
|
-
async def test_hold_key(self):
|
|
252
|
-
"""Test holding a key for duration."""
|
|
253
|
-
executor = BaseExecutor()
|
|
254
|
-
|
|
255
|
-
# Mock sleep to avoid actual wait
|
|
256
|
-
with patch("asyncio.sleep") as mock_sleep:
|
|
257
|
-
result = await executor.hold_key("shift", 0.5, take_screenshot=False)
|
|
258
|
-
|
|
259
|
-
assert isinstance(result, ContentResult)
|
|
260
|
-
assert result.output == "[SIMULATED] Hold key 'shift' for 0.5 seconds"
|
|
261
|
-
mock_sleep.assert_called_once_with(0.5)
|
|
262
|
-
|
|
263
|
-
@pytest.mark.asyncio
|
|
264
|
-
async def test_wait(self):
|
|
265
|
-
"""Test wait action."""
|
|
266
|
-
executor = BaseExecutor()
|
|
267
|
-
|
|
268
|
-
# Mock sleep to avoid actual wait
|
|
269
|
-
with patch("asyncio.sleep") as mock_sleep:
|
|
270
|
-
result = await executor.wait(1000) # 1000ms
|
|
271
|
-
|
|
272
|
-
assert isinstance(result, ContentResult)
|
|
273
|
-
assert result.output == "Waited 1000ms"
|
|
274
|
-
mock_sleep.assert_called_once_with(1.0)
|
|
275
|
-
|
|
276
|
-
@pytest.mark.asyncio
|
|
277
|
-
async def test_screenshot(self):
|
|
278
|
-
"""Test screenshot action."""
|
|
279
|
-
executor = BaseExecutor()
|
|
280
|
-
result = await executor.screenshot()
|
|
281
|
-
|
|
282
|
-
assert isinstance(result, str)
|
|
283
|
-
# Check it's a valid base64 string (starts with PNG header)
|
|
284
|
-
assert result.startswith("iVBORw0KGgo")
|
|
285
|
-
|
|
286
|
-
@pytest.mark.asyncio
|
|
287
|
-
async def test_position(self):
|
|
288
|
-
"""Test getting cursor position."""
|
|
289
|
-
executor = BaseExecutor()
|
|
290
|
-
result = await executor.position()
|
|
291
|
-
|
|
292
|
-
assert isinstance(result, ContentResult)
|
|
293
|
-
assert result.output == "[SIMULATED] Mouse position: (0, 0)"
|
|
294
|
-
|
|
295
|
-
@pytest.mark.asyncio
|
|
296
|
-
async def test_execute(self):
|
|
297
|
-
"""Test execute command."""
|
|
298
|
-
executor = BaseExecutor()
|
|
299
|
-
result = await executor.execute("custom command", take_screenshot=False)
|
|
300
|
-
|
|
301
|
-
assert isinstance(result, ContentResult)
|
|
302
|
-
assert result.output == "[SIMULATED] Execute: custom command"
|
|
303
|
-
|
|
304
|
-
@pytest.mark.asyncio
|
|
305
|
-
async def test_type_text_alias(self):
|
|
306
|
-
"""Test type_text alias method."""
|
|
307
|
-
executor = BaseExecutor()
|
|
308
|
-
result = await executor.write("test", delay=20, take_screenshot=False)
|
|
309
|
-
|
|
310
|
-
assert isinstance(result, ContentResult)
|
|
311
|
-
assert result.output == "[SIMULATED] Type 'test'"
|
|
312
|
-
|
|
313
|
-
@pytest.mark.asyncio
|
|
314
|
-
async def test_mouse_move_alias(self):
|
|
315
|
-
"""Test mouse_move alias method."""
|
|
316
|
-
executor = BaseExecutor()
|
|
317
|
-
result = await executor.mouse_move(100, 200, take_screenshot=False)
|
|
318
|
-
|
|
319
|
-
assert isinstance(result, ContentResult)
|
|
320
|
-
assert result.output == "[SIMULATED] Move mouse to (100, 200)"
|
|
321
|
-
|
|
322
|
-
@pytest.mark.asyncio
|
|
323
|
-
async def test_multiple_actions_with_screenshots(self):
|
|
324
|
-
"""Test multiple actions with screenshots to ensure consistency."""
|
|
325
|
-
executor = BaseExecutor()
|
|
326
|
-
|
|
327
|
-
# Test that screenshots are consistent
|
|
328
|
-
screenshot1 = await executor.screenshot()
|
|
329
|
-
screenshot2 = await executor.screenshot()
|
|
330
|
-
|
|
331
|
-
assert screenshot1 == screenshot2 # Simulated screenshots should be identical
|
|
332
|
-
|
|
333
|
-
# Test actions with screenshots
|
|
334
|
-
result1 = await executor.click(10, 20, take_screenshot=True)
|
|
335
|
-
result2 = await executor.write("test", take_screenshot=True)
|
|
336
|
-
|
|
337
|
-
assert result1.base64_image == screenshot1
|
|
338
|
-
assert result2.base64_image == screenshot1
|
|
1
|
+
"""Tests for BaseExecutor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hud.tools.executors.base import BaseExecutor
|
|
10
|
+
from hud.tools.types import ContentResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestBaseExecutor:
|
|
14
|
+
"""Tests for BaseExecutor simulated actions."""
|
|
15
|
+
|
|
16
|
+
def test_init(self):
|
|
17
|
+
"""Test BaseExecutor initialization."""
|
|
18
|
+
# Without display num - defaults to computer_settings.DISPLAY_NUM
|
|
19
|
+
executor = BaseExecutor()
|
|
20
|
+
assert executor.display_num == 0 # Default from computer_settings
|
|
21
|
+
assert executor._screenshot_delay == 0.5
|
|
22
|
+
|
|
23
|
+
# With display num
|
|
24
|
+
executor = BaseExecutor(display_num=1)
|
|
25
|
+
assert executor.display_num == 1
|
|
26
|
+
|
|
27
|
+
@pytest.mark.asyncio
|
|
28
|
+
async def test_click_basic(self):
|
|
29
|
+
"""Test basic click action."""
|
|
30
|
+
executor = BaseExecutor()
|
|
31
|
+
result = await executor.click(x=100, y=200, button="left", take_screenshot=False)
|
|
32
|
+
|
|
33
|
+
assert isinstance(result, ContentResult)
|
|
34
|
+
assert result.output == "[SIMULATED] Click at (100, 200) with left button"
|
|
35
|
+
assert result.base64_image is None # No screenshot requested
|
|
36
|
+
|
|
37
|
+
@pytest.mark.asyncio
|
|
38
|
+
async def test_click_with_screenshot(self):
|
|
39
|
+
"""Test click with screenshot."""
|
|
40
|
+
executor = BaseExecutor()
|
|
41
|
+
result = await executor.click(x=100, y=200, take_screenshot=True)
|
|
42
|
+
|
|
43
|
+
assert isinstance(result, ContentResult)
|
|
44
|
+
assert result.output == "[SIMULATED] Click at (100, 200) with left button"
|
|
45
|
+
assert result.base64_image is not None # Screenshot included
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_click_with_pattern(self):
|
|
49
|
+
"""Test click with multi-click pattern."""
|
|
50
|
+
executor = BaseExecutor()
|
|
51
|
+
result = await executor.click(x=100, y=200, pattern=[100, 50], take_screenshot=False)
|
|
52
|
+
|
|
53
|
+
assert isinstance(result, ContentResult)
|
|
54
|
+
assert result.output is not None
|
|
55
|
+
assert (
|
|
56
|
+
"[SIMULATED] Click at (100, 200) with left button (multi-click pattern: [100, 50])"
|
|
57
|
+
in result.output
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_click_with_hold_keys(self):
|
|
62
|
+
"""Test click while holding keys."""
|
|
63
|
+
executor = BaseExecutor()
|
|
64
|
+
result = await executor.click(
|
|
65
|
+
x=100, y=200, hold_keys=["ctrl", "shift"], take_screenshot=False
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
assert isinstance(result, ContentResult)
|
|
69
|
+
assert result.output is not None
|
|
70
|
+
assert "while holding ['ctrl', 'shift']" in result.output
|
|
71
|
+
|
|
72
|
+
@pytest.mark.asyncio
|
|
73
|
+
async def test_type_basic(self):
|
|
74
|
+
"""Test basic typing."""
|
|
75
|
+
executor = BaseExecutor()
|
|
76
|
+
result = await executor.write("Hello World", take_screenshot=False)
|
|
77
|
+
|
|
78
|
+
assert isinstance(result, ContentResult)
|
|
79
|
+
assert result.output == "[SIMULATED] Type 'Hello World'"
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_type_with_enter(self):
|
|
83
|
+
"""Test typing with enter."""
|
|
84
|
+
executor = BaseExecutor()
|
|
85
|
+
result = await executor.write("Hello", enter_after=True, take_screenshot=False)
|
|
86
|
+
|
|
87
|
+
assert isinstance(result, ContentResult)
|
|
88
|
+
assert result.output == "[SIMULATED] Type 'Hello' followed by Enter"
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_press_keys(self):
|
|
92
|
+
"""Test pressing key combination."""
|
|
93
|
+
executor = BaseExecutor()
|
|
94
|
+
result = await executor.press(["ctrl", "c"], take_screenshot=False)
|
|
95
|
+
|
|
96
|
+
assert isinstance(result, ContentResult)
|
|
97
|
+
assert result.output == "[SIMULATED] Press key combination: ctrl+c"
|
|
98
|
+
|
|
99
|
+
@pytest.mark.asyncio
|
|
100
|
+
async def test_key_single(self):
|
|
101
|
+
"""Test pressing single key."""
|
|
102
|
+
executor = BaseExecutor()
|
|
103
|
+
result = await executor.key("Return", take_screenshot=False)
|
|
104
|
+
|
|
105
|
+
assert isinstance(result, ContentResult)
|
|
106
|
+
assert result.output == "[SIMULATED] Press key: Return"
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_keydown(self):
|
|
110
|
+
"""Test key down action."""
|
|
111
|
+
executor = BaseExecutor()
|
|
112
|
+
result = await executor.keydown(["shift", "ctrl"], take_screenshot=False)
|
|
113
|
+
|
|
114
|
+
assert isinstance(result, ContentResult)
|
|
115
|
+
assert result.output == "[SIMULATED] Key down: shift, ctrl"
|
|
116
|
+
|
|
117
|
+
@pytest.mark.asyncio
|
|
118
|
+
async def test_keyup(self):
|
|
119
|
+
"""Test key up action."""
|
|
120
|
+
executor = BaseExecutor()
|
|
121
|
+
result = await executor.keyup(["shift", "ctrl"], take_screenshot=False)
|
|
122
|
+
|
|
123
|
+
assert isinstance(result, ContentResult)
|
|
124
|
+
assert result.output == "[SIMULATED] Key up: shift, ctrl"
|
|
125
|
+
|
|
126
|
+
@pytest.mark.asyncio
|
|
127
|
+
async def test_scroll_basic(self):
|
|
128
|
+
"""Test basic scroll."""
|
|
129
|
+
executor = BaseExecutor()
|
|
130
|
+
result = await executor.scroll(x=100, y=200, scroll_y=5, take_screenshot=False)
|
|
131
|
+
|
|
132
|
+
assert isinstance(result, ContentResult)
|
|
133
|
+
assert result.output is not None
|
|
134
|
+
assert "[SIMULATED] Scroll at (100, 200)" in result.output
|
|
135
|
+
assert "vertically by 5" in result.output
|
|
136
|
+
|
|
137
|
+
@pytest.mark.asyncio
|
|
138
|
+
async def test_scroll_horizontal(self):
|
|
139
|
+
"""Test horizontal scroll."""
|
|
140
|
+
executor = BaseExecutor()
|
|
141
|
+
result = await executor.scroll(scroll_x=10, take_screenshot=False)
|
|
142
|
+
|
|
143
|
+
assert isinstance(result, ContentResult)
|
|
144
|
+
assert result.output is not None
|
|
145
|
+
assert "[SIMULATED] Scroll" in result.output
|
|
146
|
+
assert "horizontally by 10" in result.output
|
|
147
|
+
|
|
148
|
+
@pytest.mark.asyncio
|
|
149
|
+
async def test_scroll_with_hold_keys(self):
|
|
150
|
+
"""Test scroll with held keys."""
|
|
151
|
+
executor = BaseExecutor()
|
|
152
|
+
result = await executor.scroll(
|
|
153
|
+
x=100, y=200, scroll_y=5, hold_keys=["shift"], take_screenshot=False
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
assert isinstance(result, ContentResult)
|
|
157
|
+
assert result.output is not None
|
|
158
|
+
assert "while holding ['shift']" in result.output
|
|
159
|
+
|
|
160
|
+
@pytest.mark.asyncio
|
|
161
|
+
async def test_move_absolute(self):
|
|
162
|
+
"""Test absolute mouse movement."""
|
|
163
|
+
executor = BaseExecutor()
|
|
164
|
+
result = await executor.move(x=300, y=400, take_screenshot=False)
|
|
165
|
+
|
|
166
|
+
assert isinstance(result, ContentResult)
|
|
167
|
+
assert result.output == "[SIMULATED] Move mouse to (300, 400)"
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
async def test_move_relative(self):
|
|
171
|
+
"""Test relative mouse movement."""
|
|
172
|
+
executor = BaseExecutor()
|
|
173
|
+
result = await executor.move(offset_x=50, offset_y=-30, take_screenshot=False)
|
|
174
|
+
|
|
175
|
+
assert isinstance(result, ContentResult)
|
|
176
|
+
assert result.output == "[SIMULATED] Move mouse by offset (50, -30)"
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_move_no_coords(self):
|
|
180
|
+
"""Test move with no coordinates."""
|
|
181
|
+
executor = BaseExecutor()
|
|
182
|
+
result = await executor.move(take_screenshot=False)
|
|
183
|
+
|
|
184
|
+
assert isinstance(result, ContentResult)
|
|
185
|
+
assert result.output == "[SIMULATED] Move mouse (no coordinates specified)"
|
|
186
|
+
|
|
187
|
+
@pytest.mark.asyncio
|
|
188
|
+
async def test_drag_basic(self):
|
|
189
|
+
"""Test basic drag operation."""
|
|
190
|
+
executor = BaseExecutor()
|
|
191
|
+
path = [(100, 100), (200, 200)]
|
|
192
|
+
result = await executor.drag(path, take_screenshot=False)
|
|
193
|
+
|
|
194
|
+
assert isinstance(result, ContentResult)
|
|
195
|
+
assert result.output == "[SIMULATED] Drag from (100, 100) to (200, 200)"
|
|
196
|
+
|
|
197
|
+
@pytest.mark.asyncio
|
|
198
|
+
async def test_drag_with_intermediate_points(self):
|
|
199
|
+
"""Test drag with intermediate points."""
|
|
200
|
+
executor = BaseExecutor()
|
|
201
|
+
path = [(100, 100), (150, 150), (200, 200)]
|
|
202
|
+
result = await executor.drag(path, take_screenshot=False)
|
|
203
|
+
|
|
204
|
+
assert isinstance(result, ContentResult)
|
|
205
|
+
assert result.output is not None
|
|
206
|
+
assert (
|
|
207
|
+
"[SIMULATED] Drag from (100, 100) to (200, 200) via 1 intermediate points"
|
|
208
|
+
in result.output
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
@pytest.mark.asyncio
|
|
212
|
+
async def test_drag_invalid_path(self):
|
|
213
|
+
"""Test drag with invalid path."""
|
|
214
|
+
executor = BaseExecutor()
|
|
215
|
+
result = await executor.drag([(100, 100)], take_screenshot=False) # Only one point
|
|
216
|
+
|
|
217
|
+
assert isinstance(result, ContentResult)
|
|
218
|
+
assert result.error == "Drag path must have at least 2 points"
|
|
219
|
+
assert result.output is None
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_drag_with_hold_keys(self):
|
|
223
|
+
"""Test drag with held keys."""
|
|
224
|
+
executor = BaseExecutor()
|
|
225
|
+
path = [(100, 100), (200, 200)]
|
|
226
|
+
result = await executor.drag(path, hold_keys=["alt"], take_screenshot=False)
|
|
227
|
+
|
|
228
|
+
assert isinstance(result, ContentResult)
|
|
229
|
+
assert result.output is not None
|
|
230
|
+
assert "while holding ['alt']" in result.output
|
|
231
|
+
|
|
232
|
+
@pytest.mark.asyncio
|
|
233
|
+
async def test_mouse_down(self):
|
|
234
|
+
"""Test mouse down action."""
|
|
235
|
+
executor = BaseExecutor()
|
|
236
|
+
result = await executor.mouse_down(button="right", take_screenshot=False)
|
|
237
|
+
|
|
238
|
+
assert isinstance(result, ContentResult)
|
|
239
|
+
assert result.output == "[SIMULATED] Mouse down: right button"
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_mouse_up(self):
|
|
243
|
+
"""Test mouse up action."""
|
|
244
|
+
executor = BaseExecutor()
|
|
245
|
+
result = await executor.mouse_up(button="middle", take_screenshot=False)
|
|
246
|
+
|
|
247
|
+
assert isinstance(result, ContentResult)
|
|
248
|
+
assert result.output == "[SIMULATED] Mouse up: middle button"
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
async def test_hold_key(self):
|
|
252
|
+
"""Test holding a key for duration."""
|
|
253
|
+
executor = BaseExecutor()
|
|
254
|
+
|
|
255
|
+
# Mock sleep to avoid actual wait
|
|
256
|
+
with patch("asyncio.sleep") as mock_sleep:
|
|
257
|
+
result = await executor.hold_key("shift", 0.5, take_screenshot=False)
|
|
258
|
+
|
|
259
|
+
assert isinstance(result, ContentResult)
|
|
260
|
+
assert result.output == "[SIMULATED] Hold key 'shift' for 0.5 seconds"
|
|
261
|
+
mock_sleep.assert_called_once_with(0.5)
|
|
262
|
+
|
|
263
|
+
@pytest.mark.asyncio
|
|
264
|
+
async def test_wait(self):
|
|
265
|
+
"""Test wait action."""
|
|
266
|
+
executor = BaseExecutor()
|
|
267
|
+
|
|
268
|
+
# Mock sleep to avoid actual wait
|
|
269
|
+
with patch("asyncio.sleep") as mock_sleep:
|
|
270
|
+
result = await executor.wait(1000) # 1000ms
|
|
271
|
+
|
|
272
|
+
assert isinstance(result, ContentResult)
|
|
273
|
+
assert result.output == "Waited 1000ms"
|
|
274
|
+
mock_sleep.assert_called_once_with(1.0)
|
|
275
|
+
|
|
276
|
+
@pytest.mark.asyncio
|
|
277
|
+
async def test_screenshot(self):
|
|
278
|
+
"""Test screenshot action."""
|
|
279
|
+
executor = BaseExecutor()
|
|
280
|
+
result = await executor.screenshot()
|
|
281
|
+
|
|
282
|
+
assert isinstance(result, str)
|
|
283
|
+
# Check it's a valid base64 string (starts with PNG header)
|
|
284
|
+
assert result.startswith("iVBORw0KGgo")
|
|
285
|
+
|
|
286
|
+
@pytest.mark.asyncio
|
|
287
|
+
async def test_position(self):
|
|
288
|
+
"""Test getting cursor position."""
|
|
289
|
+
executor = BaseExecutor()
|
|
290
|
+
result = await executor.position()
|
|
291
|
+
|
|
292
|
+
assert isinstance(result, ContentResult)
|
|
293
|
+
assert result.output == "[SIMULATED] Mouse position: (0, 0)"
|
|
294
|
+
|
|
295
|
+
@pytest.mark.asyncio
|
|
296
|
+
async def test_execute(self):
|
|
297
|
+
"""Test execute command."""
|
|
298
|
+
executor = BaseExecutor()
|
|
299
|
+
result = await executor.execute("custom command", take_screenshot=False)
|
|
300
|
+
|
|
301
|
+
assert isinstance(result, ContentResult)
|
|
302
|
+
assert result.output == "[SIMULATED] Execute: custom command"
|
|
303
|
+
|
|
304
|
+
@pytest.mark.asyncio
|
|
305
|
+
async def test_type_text_alias(self):
|
|
306
|
+
"""Test type_text alias method."""
|
|
307
|
+
executor = BaseExecutor()
|
|
308
|
+
result = await executor.write("test", delay=20, take_screenshot=False)
|
|
309
|
+
|
|
310
|
+
assert isinstance(result, ContentResult)
|
|
311
|
+
assert result.output == "[SIMULATED] Type 'test'"
|
|
312
|
+
|
|
313
|
+
@pytest.mark.asyncio
|
|
314
|
+
async def test_mouse_move_alias(self):
|
|
315
|
+
"""Test mouse_move alias method."""
|
|
316
|
+
executor = BaseExecutor()
|
|
317
|
+
result = await executor.mouse_move(100, 200, take_screenshot=False)
|
|
318
|
+
|
|
319
|
+
assert isinstance(result, ContentResult)
|
|
320
|
+
assert result.output == "[SIMULATED] Move mouse to (100, 200)"
|
|
321
|
+
|
|
322
|
+
@pytest.mark.asyncio
|
|
323
|
+
async def test_multiple_actions_with_screenshots(self):
|
|
324
|
+
"""Test multiple actions with screenshots to ensure consistency."""
|
|
325
|
+
executor = BaseExecutor()
|
|
326
|
+
|
|
327
|
+
# Test that screenshots are consistent
|
|
328
|
+
screenshot1 = await executor.screenshot()
|
|
329
|
+
screenshot2 = await executor.screenshot()
|
|
330
|
+
|
|
331
|
+
assert screenshot1 == screenshot2 # Simulated screenshots should be identical
|
|
332
|
+
|
|
333
|
+
# Test actions with screenshots
|
|
334
|
+
result1 = await executor.click(10, 20, take_screenshot=True)
|
|
335
|
+
result2 = await executor.write("test", take_screenshot=True)
|
|
336
|
+
|
|
337
|
+
assert result1.base64_image == screenshot1
|
|
338
|
+
assert result2.base64_image == screenshot1
|