hud-python 0.3.5__py3-none-any.whl → 0.4.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 (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
@@ -1,165 +1,165 @@
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 PyAutoGUIExecutor
11
-
12
- # Check if pyautogui is available for test skipping
13
- PYAUTOGUI_AVAILABLE = PyAutoGUIExecutor.is_available()
14
-
15
-
16
- class TestPyAutoGUIExecutor:
17
- """Tests for PyAutoGUIExecutor."""
18
-
19
- def test_is_available(self):
20
- """Test is_available method."""
21
- # The availability is determined by the module-level PYAUTOGUI_AVAILABLE
22
- assert PyAutoGUIExecutor.is_available() == PYAUTOGUI_AVAILABLE
23
-
24
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
25
- @pytest.mark.asyncio
26
- async def test_screenshot_with_pyautogui(self):
27
- """Test screenshot when pyautogui is available."""
28
- executor = PyAutoGUIExecutor()
29
-
30
- # Mock pyautogui screenshot
31
- with patch("pyautogui.screenshot") as mock_screenshot:
32
- mock_img = MagicMock()
33
- mock_img.save = MagicMock()
34
- mock_screenshot.return_value = mock_img
35
-
36
- result = await executor.screenshot()
37
-
38
- # screenshot() returns a base64 string, not a ToolResult
39
- assert isinstance(result, str)
40
- mock_screenshot.assert_called_once()
41
-
42
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
43
- @pytest.mark.asyncio
44
- async def test_click_with_pyautogui(self):
45
- """Test click when pyautogui is available."""
46
- executor = PyAutoGUIExecutor()
47
-
48
- with patch("pyautogui.click") as mock_click:
49
- result = await executor.click(100, 200, "left")
50
-
51
- assert isinstance(result, ToolResult)
52
- assert result.output and "Clicked" in result.output
53
- mock_click.assert_called_once_with(x=100, y=200, button="left")
54
-
55
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
56
- @pytest.mark.asyncio
57
- async def test_type_text_with_pyautogui(self):
58
- """Test type when pyautogui is available."""
59
- executor = PyAutoGUIExecutor()
60
-
61
- with patch("pyautogui.typewrite") as mock_type:
62
- result = await executor.type("Hello world")
63
-
64
- assert isinstance(result, ToolResult)
65
- assert result.output and "Typed" in result.output
66
- # The implementation adds interval=0.012 (12ms converted to seconds)
67
- mock_type.assert_called_once_with("Hello world", interval=0.012)
68
-
69
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
70
- @pytest.mark.asyncio
71
- async def test_press_keys_with_pyautogui(self):
72
- """Test press when pyautogui is available."""
73
- executor = PyAutoGUIExecutor()
74
-
75
- # For key combinations, the implementation uses hotkey
76
- with patch("pyautogui.hotkey") as mock_hotkey:
77
- result = await executor.press(["ctrl", "a"])
78
-
79
- assert isinstance(result, ToolResult)
80
- assert result.output and "Pressed" in result.output
81
- mock_hotkey.assert_called_once_with("ctrl", "a")
82
-
83
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
84
- @pytest.mark.asyncio
85
- async def test_scroll_with_pyautogui(self):
86
- """Test scroll when pyautogui is available."""
87
- executor = PyAutoGUIExecutor()
88
-
89
- with patch("pyautogui.moveTo") as mock_move, patch("pyautogui.scroll") as mock_scroll:
90
- result = await executor.scroll(100, 200, scroll_y=5)
91
-
92
- assert isinstance(result, ToolResult)
93
- assert result.output and "Scrolled" in result.output
94
- # First moves to position
95
- mock_move.assert_called_once_with(100, 200)
96
- # Then scrolls (note: implementation negates scroll_y)
97
- mock_scroll.assert_called_once_with(-5)
98
-
99
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
100
- @pytest.mark.asyncio
101
- async def test_move_with_pyautogui(self):
102
- """Test move when pyautogui is available."""
103
- executor = PyAutoGUIExecutor()
104
-
105
- with patch("pyautogui.moveTo") as mock_move:
106
- result = await executor.move(300, 400)
107
-
108
- assert isinstance(result, ToolResult)
109
- assert result.output and "Moved" in result.output
110
- # The implementation adds duration=0.1
111
- mock_move.assert_called_once_with(300, 400, duration=0.1)
112
-
113
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
114
- @pytest.mark.asyncio
115
- async def test_drag_with_pyautogui(self):
116
- """Test drag when pyautogui is available."""
117
- executor = PyAutoGUIExecutor()
118
-
119
- with patch("pyautogui.dragTo") as mock_drag:
120
- # drag expects a path (list of coordinate tuples)
121
- path = [(100, 100), (300, 400)]
122
- result = await executor.drag(path)
123
-
124
- assert isinstance(result, ToolResult)
125
- assert result.output and "Dragged" in result.output
126
- # Implementation uses dragTo to move to each point
127
- mock_drag.assert_called()
128
-
129
- @pytest.mark.asyncio
130
- async def test_wait(self):
131
- """Test wait method."""
132
- executor = PyAutoGUIExecutor()
133
-
134
- # Mock asyncio.sleep
135
- with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
136
- # wait expects time in milliseconds
137
- result = await executor.wait(2500) # 2500ms = 2.5s
138
-
139
- assert isinstance(result, ToolResult)
140
- assert result.output and "Waited" in result.output
141
- # Implementation converts to seconds
142
- mock_sleep.assert_called_once_with(2.5)
143
-
144
- @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
145
- @pytest.mark.asyncio
146
- async def test_position_with_pyautogui(self):
147
- """Test position when pyautogui is available."""
148
- executor = PyAutoGUIExecutor()
149
-
150
- with patch("pyautogui.position") as mock_position:
151
- mock_position.return_value = (123, 456)
152
- result = await executor.position()
153
-
154
- assert isinstance(result, ToolResult)
155
- assert result.output is not None
156
- assert "Mouse position" in result.output
157
- assert "123" in result.output
158
- assert "456" in result.output
159
- mock_position.assert_called_once()
160
-
161
- def test_init_with_display_num(self):
162
- """Test initialization with display number."""
163
- # Should not raise
164
- executor = PyAutoGUIExecutor(display_num=0)
165
- assert executor.display_num == 0
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.executors.pyautogui import PyAutoGUIExecutor
10
+ from hud.tools.types import ContentResult
11
+
12
+ # Check if pyautogui is available for test skipping
13
+ PYAUTOGUI_AVAILABLE = PyAutoGUIExecutor.is_available()
14
+
15
+
16
+ class TestPyAutoGUIExecutor:
17
+ """Tests for PyAutoGUIExecutor."""
18
+
19
+ def test_is_available(self):
20
+ """Test is_available method."""
21
+ # The availability is determined by the module-level PYAUTOGUI_AVAILABLE
22
+ assert PyAutoGUIExecutor.is_available() == PYAUTOGUI_AVAILABLE
23
+
24
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
25
+ @pytest.mark.asyncio
26
+ async def test_screenshot_with_pyautogui(self):
27
+ """Test screenshot when pyautogui is available."""
28
+ executor = PyAutoGUIExecutor()
29
+
30
+ # Mock pyautogui screenshot
31
+ with patch("pyautogui.screenshot") as mock_screenshot:
32
+ mock_img = MagicMock()
33
+ mock_img.save = MagicMock()
34
+ mock_screenshot.return_value = mock_img
35
+
36
+ result = await executor.screenshot()
37
+
38
+ # screenshot() returns a base64 string, not a ContentResult
39
+ assert isinstance(result, str)
40
+ mock_screenshot.assert_called_once()
41
+
42
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
43
+ @pytest.mark.asyncio
44
+ async def test_click_with_pyautogui(self):
45
+ """Test click when pyautogui is available."""
46
+ executor = PyAutoGUIExecutor()
47
+
48
+ with patch("pyautogui.click") as mock_click:
49
+ result = await executor.click(100, 200, "left")
50
+
51
+ assert isinstance(result, ContentResult)
52
+ assert result.output and "Clicked" in result.output
53
+ mock_click.assert_called_once_with(x=100, y=200, button="left")
54
+
55
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
56
+ @pytest.mark.asyncio
57
+ async def test_type_text_with_pyautogui(self):
58
+ """Test type when pyautogui is available."""
59
+ executor = PyAutoGUIExecutor()
60
+
61
+ with patch("pyautogui.typewrite") as mock_type:
62
+ result = await executor.write("Hello world")
63
+
64
+ assert isinstance(result, ContentResult)
65
+ assert result.output and "Typed" in result.output
66
+ # The implementation adds interval=0.012 (12ms converted to seconds)
67
+ mock_type.assert_called_once_with("Hello world", interval=0.012)
68
+
69
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
70
+ @pytest.mark.asyncio
71
+ async def test_press_keys_with_pyautogui(self):
72
+ """Test press when pyautogui is available."""
73
+ executor = PyAutoGUIExecutor()
74
+
75
+ # For key combinations, the implementation uses hotkey
76
+ with patch("pyautogui.hotkey") as mock_hotkey:
77
+ result = await executor.press(["ctrl", "a"])
78
+
79
+ assert isinstance(result, ContentResult)
80
+ assert result.output and "Pressed" in result.output
81
+ mock_hotkey.assert_called_once_with("ctrl", "a")
82
+
83
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
84
+ @pytest.mark.asyncio
85
+ async def test_scroll_with_pyautogui(self):
86
+ """Test scroll when pyautogui is available."""
87
+ executor = PyAutoGUIExecutor()
88
+
89
+ with patch("pyautogui.moveTo") as mock_move, patch("pyautogui.scroll") as mock_scroll:
90
+ result = await executor.scroll(100, 200, scroll_y=5)
91
+
92
+ assert isinstance(result, ContentResult)
93
+ assert result.output and "Scrolled" in result.output
94
+ # First moves to position
95
+ mock_move.assert_called_once_with(100, 200)
96
+ # Then scrolls (note: implementation negates scroll_y)
97
+ mock_scroll.assert_called_once_with(-5)
98
+
99
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
100
+ @pytest.mark.asyncio
101
+ async def test_move_with_pyautogui(self):
102
+ """Test move when pyautogui is available."""
103
+ executor = PyAutoGUIExecutor()
104
+
105
+ with patch("pyautogui.moveTo") as mock_move:
106
+ result = await executor.move(300, 400)
107
+
108
+ assert isinstance(result, ContentResult)
109
+ assert result.output and "Moved" in result.output
110
+ # The implementation adds duration=0.1
111
+ mock_move.assert_called_once_with(300, 400, duration=0.1)
112
+
113
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
114
+ @pytest.mark.asyncio
115
+ async def test_drag_with_pyautogui(self):
116
+ """Test drag when pyautogui is available."""
117
+ executor = PyAutoGUIExecutor()
118
+
119
+ with patch("pyautogui.dragTo") as mock_drag:
120
+ # drag expects a path (list of coordinate tuples)
121
+ path = [(100, 100), (300, 400)]
122
+ result = await executor.drag(path)
123
+
124
+ assert isinstance(result, ContentResult)
125
+ assert result.output and "Dragged" in result.output
126
+ # Implementation uses dragTo to move to each point
127
+ mock_drag.assert_called()
128
+
129
+ @pytest.mark.asyncio
130
+ async def test_wait(self):
131
+ """Test wait method."""
132
+ executor = PyAutoGUIExecutor()
133
+
134
+ # Mock asyncio.sleep
135
+ with patch("asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
136
+ # wait expects time in milliseconds
137
+ result = await executor.wait(2500) # 2500ms = 2.5s
138
+
139
+ assert isinstance(result, ContentResult)
140
+ assert result.output and "Waited" in result.output
141
+ # Implementation converts to seconds
142
+ mock_sleep.assert_called_once_with(2.5)
143
+
144
+ @pytest.mark.skipif(not PYAUTOGUI_AVAILABLE, reason="pyautogui not available")
145
+ @pytest.mark.asyncio
146
+ async def test_position_with_pyautogui(self):
147
+ """Test position when pyautogui is available."""
148
+ executor = PyAutoGUIExecutor()
149
+
150
+ with patch("pyautogui.position") as mock_position:
151
+ mock_position.return_value = (123, 456)
152
+ result = await executor.position()
153
+
154
+ assert isinstance(result, ContentResult)
155
+ assert result.output is not None
156
+ assert "Mouse position" in result.output
157
+ assert "123" in result.output
158
+ assert "456" in result.output
159
+ mock_position.assert_called_once()
160
+
161
+ def test_init_with_display_num(self):
162
+ """Test initialization with display number."""
163
+ # Should not raise
164
+ executor = PyAutoGUIExecutor(display_num=0)
165
+ assert executor.display_num == 0