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