hud-python 0.2.10__py3-none-any.whl → 0.3.0__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 (64) hide show
  1. hud/__init__.py +14 -5
  2. hud/env/docker_client.py +1 -1
  3. hud/env/environment.py +10 -7
  4. hud/env/local_docker_client.py +1 -1
  5. hud/env/remote_client.py +1 -1
  6. hud/env/remote_docker_client.py +2 -2
  7. hud/exceptions.py +2 -1
  8. hud/mcp_agent/__init__.py +15 -0
  9. hud/mcp_agent/base.py +723 -0
  10. hud/mcp_agent/claude.py +316 -0
  11. hud/mcp_agent/langchain.py +231 -0
  12. hud/mcp_agent/openai.py +318 -0
  13. hud/mcp_agent/tests/__init__.py +1 -0
  14. hud/mcp_agent/tests/test_base.py +437 -0
  15. hud/settings.py +14 -2
  16. hud/task.py +4 -0
  17. hud/telemetry/__init__.py +11 -7
  18. hud/telemetry/_trace.py +82 -71
  19. hud/telemetry/context.py +9 -27
  20. hud/telemetry/exporter.py +6 -5
  21. hud/telemetry/instrumentation/mcp.py +174 -410
  22. hud/telemetry/mcp_models.py +13 -74
  23. hud/telemetry/tests/test_context.py +9 -6
  24. hud/telemetry/tests/test_trace.py +92 -61
  25. hud/tools/__init__.py +21 -0
  26. hud/tools/base.py +65 -0
  27. hud/tools/bash.py +137 -0
  28. hud/tools/computer/__init__.py +13 -0
  29. hud/tools/computer/anthropic.py +411 -0
  30. hud/tools/computer/hud.py +315 -0
  31. hud/tools/computer/openai.py +283 -0
  32. hud/tools/edit.py +290 -0
  33. hud/tools/executors/__init__.py +13 -0
  34. hud/tools/executors/base.py +331 -0
  35. hud/tools/executors/pyautogui.py +585 -0
  36. hud/tools/executors/tests/__init__.py +1 -0
  37. hud/tools/executors/tests/test_base_executor.py +338 -0
  38. hud/tools/executors/tests/test_pyautogui_executor.py +162 -0
  39. hud/tools/executors/xdo.py +503 -0
  40. hud/tools/helper/README.md +56 -0
  41. hud/tools/helper/__init__.py +9 -0
  42. hud/tools/helper/mcp_server.py +78 -0
  43. hud/tools/helper/server_initialization.py +115 -0
  44. hud/tools/helper/utils.py +58 -0
  45. hud/tools/playwright_tool.py +373 -0
  46. hud/tools/tests/__init__.py +3 -0
  47. hud/tools/tests/test_bash.py +152 -0
  48. hud/tools/tests/test_computer.py +52 -0
  49. hud/tools/tests/test_computer_actions.py +34 -0
  50. hud/tools/tests/test_edit.py +233 -0
  51. hud/tools/tests/test_init.py +27 -0
  52. hud/tools/tests/test_playwright_tool.py +183 -0
  53. hud/tools/tests/test_tools.py +154 -0
  54. hud/tools/tests/test_utils.py +156 -0
  55. hud/tools/utils.py +50 -0
  56. hud/types.py +10 -1
  57. hud/utils/tests/test_init.py +21 -0
  58. hud/utils/tests/test_version.py +1 -1
  59. hud/version.py +1 -1
  60. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/METADATA +9 -6
  61. hud_python-0.3.0.dist-info/RECORD +124 -0
  62. hud_python-0.2.10.dist-info/RECORD +0 -85
  63. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/WHEEL +0 -0
  64. {hud_python-0.2.10.dist-info → hud_python-0.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +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.base import ToolResult
10
+ from hud.tools.executors.base import BaseExecutor
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
19
+ executor = BaseExecutor()
20
+ assert executor.display_num is None
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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.type("Hello World", take_screenshot=False)
77
+
78
+ assert isinstance(result, ToolResult)
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.type("Hello", enter_after=True, take_screenshot=False)
86
+
87
+ assert isinstance(result, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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, ToolResult)
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.type_text("test", delay=20, take_screenshot=False)
309
+
310
+ assert isinstance(result, ToolResult)
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, ToolResult)
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.type("test", take_screenshot=True)
336
+
337
+ assert result1.base64_image == screenshot1
338
+ assert result2.base64_image == screenshot1
@@ -0,0 +1,162 @@
1
+ """Tests for PyAutoGUI executor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, MagicMock, patch
6
+
7
+ import pytest
8
+
9
+ from hud.tools.base import ToolResult
10
+ from hud.tools.executors.pyautogui import PYAUTOGUI_AVAILABLE, PyAutoGUIExecutor
11
+
12
+
13
+ class TestPyAutoGUIExecutor:
14
+ """Tests for PyAutoGUIExecutor."""
15
+
16
+ def test_is_available(self):
17
+ """Test is_available method."""
18
+ # The availability is determined by the module-level PYAUTOGUI_AVAILABLE
19
+ assert PyAutoGUIExecutor.is_available() == PYAUTOGUI_AVAILABLE
20
+
21
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
22
+ @pytest.mark.asyncio
23
+ async def test_screenshot_with_pyautogui(self):
24
+ """Test screenshot when pyautogui is available."""
25
+ executor = PyAutoGUIExecutor()
26
+
27
+ # Mock pyautogui screenshot
28
+ with patch("pyautogui.screenshot") as mock_screenshot:
29
+ mock_img = MagicMock()
30
+ mock_img.save = MagicMock()
31
+ mock_screenshot.return_value = mock_img
32
+
33
+ result = await executor.screenshot()
34
+
35
+ # screenshot() returns a base64 string, not a ToolResult
36
+ assert isinstance(result, str)
37
+ mock_screenshot.assert_called_once()
38
+
39
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
40
+ @pytest.mark.asyncio
41
+ async def test_click_with_pyautogui(self):
42
+ """Test click when pyautogui is available."""
43
+ executor = PyAutoGUIExecutor()
44
+
45
+ with patch("pyautogui.click") as mock_click:
46
+ result = await executor.click(100, 200, "left")
47
+
48
+ assert isinstance(result, ToolResult)
49
+ assert result.output and "Clicked" in result.output
50
+ mock_click.assert_called_once_with(x=100, y=200, button="left")
51
+
52
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
53
+ @pytest.mark.asyncio
54
+ async def test_type_text_with_pyautogui(self):
55
+ """Test type when pyautogui is available."""
56
+ executor = PyAutoGUIExecutor()
57
+
58
+ with patch("pyautogui.typewrite") as mock_type:
59
+ result = await executor.type("Hello world")
60
+
61
+ assert isinstance(result, ToolResult)
62
+ assert result.output and "Typed" in result.output
63
+ # The implementation adds interval=0.012 (12ms converted to seconds)
64
+ mock_type.assert_called_once_with("Hello world", interval=0.012)
65
+
66
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
67
+ @pytest.mark.asyncio
68
+ async def test_press_keys_with_pyautogui(self):
69
+ """Test press when pyautogui is available."""
70
+ executor = PyAutoGUIExecutor()
71
+
72
+ # For key combinations, the implementation uses hotkey
73
+ with patch("pyautogui.hotkey") as mock_hotkey:
74
+ result = await executor.press(["ctrl", "a"])
75
+
76
+ assert isinstance(result, ToolResult)
77
+ assert result.output and "Pressed" in result.output
78
+ mock_hotkey.assert_called_once_with("ctrl", "a")
79
+
80
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
81
+ @pytest.mark.asyncio
82
+ async def test_scroll_with_pyautogui(self):
83
+ """Test scroll when pyautogui is available."""
84
+ executor = PyAutoGUIExecutor()
85
+
86
+ with patch("pyautogui.moveTo") as mock_move, patch("pyautogui.scroll") as mock_scroll:
87
+ result = await executor.scroll(100, 200, scroll_y=5)
88
+
89
+ assert isinstance(result, ToolResult)
90
+ assert result.output and "Scrolled" in result.output
91
+ # First moves to position
92
+ mock_move.assert_called_once_with(100, 200)
93
+ # Then scrolls (note: implementation negates scroll_y)
94
+ mock_scroll.assert_called_once_with(-5)
95
+
96
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
97
+ @pytest.mark.asyncio
98
+ async def test_move_with_pyautogui(self):
99
+ """Test move when pyautogui is available."""
100
+ executor = PyAutoGUIExecutor()
101
+
102
+ with patch("pyautogui.moveTo") as mock_move:
103
+ result = await executor.move(300, 400)
104
+
105
+ assert isinstance(result, ToolResult)
106
+ assert result.output and "Moved" in result.output
107
+ # The implementation adds duration=0.1
108
+ mock_move.assert_called_once_with(300, 400, duration=0.1)
109
+
110
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
111
+ @pytest.mark.asyncio
112
+ async def test_drag_with_pyautogui(self):
113
+ """Test drag when pyautogui is available."""
114
+ executor = PyAutoGUIExecutor()
115
+
116
+ with patch("pyautogui.dragTo") as mock_drag:
117
+ # drag expects a path (list of coordinate tuples)
118
+ path = [(100, 100), (300, 400)]
119
+ result = await executor.drag(path)
120
+
121
+ assert isinstance(result, ToolResult)
122
+ assert result.output and "Dragged" in result.output
123
+ # Implementation uses dragTo to move to each point
124
+ mock_drag.assert_called()
125
+
126
+ @pytest.mark.asyncio
127
+ async def test_wait(self):
128
+ """Test wait method."""
129
+ executor = PyAutoGUIExecutor()
130
+
131
+ # Mock asyncio.sleep
132
+ with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
133
+ # wait expects time in milliseconds
134
+ result = await executor.wait(2500) # 2500ms = 2.5s
135
+
136
+ assert isinstance(result, ToolResult)
137
+ assert result.output and "Waited" in result.output
138
+ # Implementation converts to seconds
139
+ mock_sleep.assert_called_once_with(2.5)
140
+
141
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
142
+ @pytest.mark.asyncio
143
+ async def test_position_with_pyautogui(self):
144
+ """Test position when pyautogui is available."""
145
+ executor = PyAutoGUIExecutor()
146
+
147
+ with patch("pyautogui.position") as mock_position:
148
+ mock_position.return_value = (123, 456)
149
+ result = await executor.position()
150
+
151
+ assert isinstance(result, ToolResult)
152
+ assert result.output is not None
153
+ assert "Mouse position" in result.output
154
+ assert "123" in result.output
155
+ assert "456" in result.output
156
+ mock_position.assert_called_once()
157
+
158
+ def test_init_with_display_num(self):
159
+ """Test initialization with display number."""
160
+ # Should not raise
161
+ executor = PyAutoGUIExecutor(display_num=0)
162
+ assert executor.display_num == 0