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,425 +1,425 @@
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
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