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.

Files changed (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {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