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,183 +1,183 @@
1
- """Tests for Playwright tool."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import AsyncMock, patch
6
-
7
- import pytest
8
- from mcp.shared.exceptions import McpError
9
- from mcp.types import INVALID_PARAMS, ImageContent, TextContent
10
-
11
- from hud.tools.playwright import PlaywrightTool
12
-
13
-
14
- class TestPlaywrightTool:
15
- """Tests for PlaywrightTool."""
16
-
17
- @pytest.mark.asyncio
18
- async def test_playwright_tool_init(self):
19
- """Test tool initialization."""
20
- tool = PlaywrightTool()
21
- assert tool._browser is None
22
- assert tool._browser_context is None
23
- assert tool.page is None
24
-
25
- @pytest.mark.asyncio
26
- async def test_playwright_tool_invalid_action(self):
27
- """Test that invalid action raises error."""
28
- tool = PlaywrightTool()
29
-
30
- with pytest.raises(McpError) as exc_info:
31
- await tool(action="invalid_action")
32
-
33
- assert exc_info.value.error.code == INVALID_PARAMS
34
- assert "Unknown action" in exc_info.value.error.message
35
-
36
- @pytest.mark.asyncio
37
- async def test_playwright_tool_navigate_with_mocked_browser(self):
38
- """Test navigate action with mocked browser."""
39
- tool = PlaywrightTool()
40
-
41
- # Mock the browser components
42
- mock_page = AsyncMock()
43
- mock_page.goto = AsyncMock()
44
-
45
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock) as mock_ensure:
46
- # Set up the tool with mocked page
47
- tool.page = mock_page
48
-
49
- blocks = await tool(action="navigate", url="https://example.com")
50
-
51
- assert blocks is not None
52
- assert any(isinstance(b, TextContent) for b in blocks)
53
- # The actual call includes wait_until parameter with a Field object
54
- mock_page.goto.assert_called_once()
55
- args, kwargs = mock_page.goto.call_args
56
- assert args[0] == "https://example.com"
57
- mock_ensure.assert_called_once()
58
-
59
- @pytest.mark.asyncio
60
- async def test_playwright_tool_click_with_mocked_browser(self):
61
- """Test click action with mocked browser."""
62
- tool = PlaywrightTool()
63
-
64
- # Mock the browser components
65
- mock_page = AsyncMock()
66
- mock_page.click = AsyncMock()
67
-
68
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
69
- # Set up the tool with mocked page
70
- tool.page = mock_page
71
-
72
- blocks = await tool(action="click", selector="button#submit")
73
-
74
- assert blocks is not None
75
- assert any(isinstance(b, TextContent) for b in blocks)
76
- mock_page.click.assert_called_once_with("button#submit", button="left", click_count=1)
77
-
78
- @pytest.mark.asyncio
79
- async def test_playwright_tool_type_with_mocked_browser(self):
80
- """Test type action with mocked browser."""
81
- tool = PlaywrightTool()
82
-
83
- # Mock the browser components
84
- mock_page = AsyncMock()
85
- mock_page.fill = AsyncMock() # Playwright uses fill, not type
86
-
87
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
88
- # Set up the tool with mocked page
89
- tool.page = mock_page
90
-
91
- blocks = await tool(action="type", selector="input#name", text="John Doe")
92
-
93
- assert blocks is not None
94
- assert any(isinstance(b, TextContent) for b in blocks)
95
- mock_page.fill.assert_called_once_with("input#name", "John Doe")
96
-
97
- @pytest.mark.asyncio
98
- async def test_playwright_tool_screenshot_with_mocked_browser(self):
99
- """Test screenshot action with mocked browser."""
100
- tool = PlaywrightTool()
101
-
102
- # Mock the browser components
103
- mock_page = AsyncMock()
104
- mock_page.screenshot = AsyncMock(return_value=b"fake_screenshot_data")
105
-
106
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
107
- # Set up the tool with mocked page
108
- tool.page = mock_page
109
-
110
- blocks = await tool(action="screenshot")
111
-
112
- assert blocks is not None
113
- assert len(blocks) > 0
114
- assert any(isinstance(b, ImageContent | TextContent) for b in blocks)
115
- mock_page.screenshot.assert_called_once()
116
-
117
- @pytest.mark.asyncio
118
- async def test_playwright_tool_get_page_info_with_mocked_browser(self):
119
- """Test get_page_info action with mocked browser."""
120
- tool = PlaywrightTool()
121
-
122
- # Mock the browser components
123
- mock_page = AsyncMock()
124
- mock_page.url = "https://example.com"
125
- mock_page.title = AsyncMock(return_value="Example Page")
126
- mock_page.evaluate = AsyncMock(return_value={"height": 1000})
127
-
128
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
129
- # Set up the tool with mocked page
130
- tool.page = mock_page
131
-
132
- blocks = await tool(action="get_page_info")
133
-
134
- assert blocks is not None
135
- assert any(isinstance(b, TextContent) for b in blocks)
136
- # Check that the text contains expected info
137
- text_blocks = [b.text for b in blocks if isinstance(b, TextContent)]
138
- combined_text = " ".join(text_blocks)
139
- assert "https://example.com" in combined_text
140
- assert "Example Page" in combined_text
141
-
142
- @pytest.mark.asyncio
143
- async def test_playwright_tool_wait_for_element_with_mocked_browser(self):
144
- """Test wait_for_element action with mocked browser."""
145
- tool = PlaywrightTool()
146
-
147
- # Mock the browser components
148
- mock_page = AsyncMock()
149
- mock_page.wait_for_selector = AsyncMock()
150
-
151
- with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
152
- # Set up the tool with mocked page
153
- tool.page = mock_page
154
-
155
- # wait_for_element doesn't accept timeout parameter directly
156
- blocks = await tool(action="wait_for_element", selector="div#loaded")
157
-
158
- assert blocks is not None
159
- assert any(isinstance(b, TextContent) for b in blocks)
160
- # Default timeout is used
161
- mock_page.wait_for_selector.assert_called_once()
162
-
163
- @pytest.mark.asyncio
164
- async def test_playwright_tool_cleanup(self):
165
- """Test cleanup functionality."""
166
- tool = PlaywrightTool()
167
-
168
- # Mock browser and context
169
- mock_browser = AsyncMock()
170
- mock_context = AsyncMock()
171
- mock_page = AsyncMock()
172
-
173
- tool._browser = mock_browser
174
- tool._browser_context = mock_context
175
- tool.page = mock_page
176
-
177
- # Call the cleanup method directly (tool is not a context manager)
178
- await tool.close()
179
-
180
- mock_browser.close.assert_called_once()
181
- assert tool._browser is None
182
- assert tool._browser_context is None
183
- assert tool.page is None
1
+ """Tests for Playwright tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, patch
6
+
7
+ import pytest
8
+ from mcp.shared.exceptions import McpError
9
+ from mcp.types import INVALID_PARAMS, ImageContent, TextContent
10
+
11
+ from hud.tools.playwright import PlaywrightTool
12
+
13
+
14
+ class TestPlaywrightTool:
15
+ """Tests for PlaywrightTool."""
16
+
17
+ @pytest.mark.asyncio
18
+ async def test_playwright_tool_init(self):
19
+ """Test tool initialization."""
20
+ tool = PlaywrightTool()
21
+ assert tool._browser is None
22
+ assert tool._browser_context is None
23
+ assert tool.page is None
24
+
25
+ @pytest.mark.asyncio
26
+ async def test_playwright_tool_invalid_action(self):
27
+ """Test that invalid action raises error."""
28
+ tool = PlaywrightTool()
29
+
30
+ with pytest.raises(McpError) as exc_info:
31
+ await tool(action="invalid_action")
32
+
33
+ assert exc_info.value.error.code == INVALID_PARAMS
34
+ assert "Unknown action" in exc_info.value.error.message
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_playwright_tool_navigate_with_mocked_browser(self):
38
+ """Test navigate action with mocked browser."""
39
+ tool = PlaywrightTool()
40
+
41
+ # Mock the browser components
42
+ mock_page = AsyncMock()
43
+ mock_page.goto = AsyncMock()
44
+
45
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock) as mock_ensure:
46
+ # Set up the tool with mocked page
47
+ tool.page = mock_page
48
+
49
+ blocks = await tool(action="navigate", url="https://example.com")
50
+
51
+ assert blocks is not None
52
+ assert any(isinstance(b, TextContent) for b in blocks)
53
+ # The actual call includes wait_until parameter with a Field object
54
+ mock_page.goto.assert_called_once()
55
+ args, kwargs = mock_page.goto.call_args
56
+ assert args[0] == "https://example.com"
57
+ mock_ensure.assert_called_once()
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_playwright_tool_click_with_mocked_browser(self):
61
+ """Test click action with mocked browser."""
62
+ tool = PlaywrightTool()
63
+
64
+ # Mock the browser components
65
+ mock_page = AsyncMock()
66
+ mock_page.click = AsyncMock()
67
+
68
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
69
+ # Set up the tool with mocked page
70
+ tool.page = mock_page
71
+
72
+ blocks = await tool(action="click", selector="button#submit")
73
+
74
+ assert blocks is not None
75
+ assert any(isinstance(b, TextContent) for b in blocks)
76
+ mock_page.click.assert_called_once_with("button#submit", button="left", click_count=1)
77
+
78
+ @pytest.mark.asyncio
79
+ async def test_playwright_tool_type_with_mocked_browser(self):
80
+ """Test type action with mocked browser."""
81
+ tool = PlaywrightTool()
82
+
83
+ # Mock the browser components
84
+ mock_page = AsyncMock()
85
+ mock_page.fill = AsyncMock() # Playwright uses fill, not type
86
+
87
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
88
+ # Set up the tool with mocked page
89
+ tool.page = mock_page
90
+
91
+ blocks = await tool(action="type", selector="input#name", text="John Doe")
92
+
93
+ assert blocks is not None
94
+ assert any(isinstance(b, TextContent) for b in blocks)
95
+ mock_page.fill.assert_called_once_with("input#name", "John Doe")
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_playwright_tool_screenshot_with_mocked_browser(self):
99
+ """Test screenshot action with mocked browser."""
100
+ tool = PlaywrightTool()
101
+
102
+ # Mock the browser components
103
+ mock_page = AsyncMock()
104
+ mock_page.screenshot = AsyncMock(return_value=b"fake_screenshot_data")
105
+
106
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
107
+ # Set up the tool with mocked page
108
+ tool.page = mock_page
109
+
110
+ blocks = await tool(action="screenshot")
111
+
112
+ assert blocks is not None
113
+ assert len(blocks) > 0
114
+ assert any(isinstance(b, ImageContent | TextContent) for b in blocks)
115
+ mock_page.screenshot.assert_called_once()
116
+
117
+ @pytest.mark.asyncio
118
+ async def test_playwright_tool_get_page_info_with_mocked_browser(self):
119
+ """Test get_page_info action with mocked browser."""
120
+ tool = PlaywrightTool()
121
+
122
+ # Mock the browser components
123
+ mock_page = AsyncMock()
124
+ mock_page.url = "https://example.com"
125
+ mock_page.title = AsyncMock(return_value="Example Page")
126
+ mock_page.evaluate = AsyncMock(return_value={"height": 1000})
127
+
128
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
129
+ # Set up the tool with mocked page
130
+ tool.page = mock_page
131
+
132
+ blocks = await tool(action="get_page_info")
133
+
134
+ assert blocks is not None
135
+ assert any(isinstance(b, TextContent) for b in blocks)
136
+ # Check that the text contains expected info
137
+ text_blocks = [b.text for b in blocks if isinstance(b, TextContent)]
138
+ combined_text = " ".join(text_blocks)
139
+ assert "https://example.com" in combined_text
140
+ assert "Example Page" in combined_text
141
+
142
+ @pytest.mark.asyncio
143
+ async def test_playwright_tool_wait_for_element_with_mocked_browser(self):
144
+ """Test wait_for_element action with mocked browser."""
145
+ tool = PlaywrightTool()
146
+
147
+ # Mock the browser components
148
+ mock_page = AsyncMock()
149
+ mock_page.wait_for_selector = AsyncMock()
150
+
151
+ with patch.object(tool, "_ensure_browser", new_callable=AsyncMock):
152
+ # Set up the tool with mocked page
153
+ tool.page = mock_page
154
+
155
+ # wait_for_element doesn't accept timeout parameter directly
156
+ blocks = await tool(action="wait_for_element", selector="div#loaded")
157
+
158
+ assert blocks is not None
159
+ assert any(isinstance(b, TextContent) for b in blocks)
160
+ # Default timeout is used
161
+ mock_page.wait_for_selector.assert_called_once()
162
+
163
+ @pytest.mark.asyncio
164
+ async def test_playwright_tool_cleanup(self):
165
+ """Test cleanup functionality."""
166
+ tool = PlaywrightTool()
167
+
168
+ # Mock browser and context
169
+ mock_browser = AsyncMock()
170
+ mock_context = AsyncMock()
171
+ mock_page = AsyncMock()
172
+
173
+ tool._browser = mock_browser
174
+ tool._browser_context = mock_context
175
+ tool.page = mock_page
176
+
177
+ # Call the cleanup method directly (tool is not a context manager)
178
+ await tool.close()
179
+
180
+ mock_browser.close.assert_called_once()
181
+ assert tool._browser is None
182
+ assert tool._browser_context is None
183
+ assert tool.page is None
@@ -1,145 +1,145 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
-
5
- import pytest
6
- from mcp.types import ImageContent, TextContent
7
-
8
- from hud.tools.bash import BashTool
9
- from hud.tools.computer.hud import HudComputerTool
10
- from hud.tools.edit import EditTool
11
-
12
-
13
- @pytest.mark.asyncio
14
- async def test_bash_tool_echo():
15
- tool = BashTool()
16
-
17
- # Monkey-patch the private _session methods so no subprocess is spawned
18
- from hud.tools.types import ContentResult
19
-
20
- class _FakeSession:
21
- async def run(self, cmd: str):
22
- return ContentResult(output=f"mocked: {cmd}")
23
-
24
- async def start(self):
25
- return None
26
-
27
- tool.session = _FakeSession() # type: ignore[assignment]
28
-
29
- result = await tool(command="echo hello")
30
- assert len(result) > 0
31
- assert isinstance(result[0], TextContent)
32
- assert result[0].text == "mocked: echo hello"
33
-
34
-
35
- @pytest.mark.asyncio
36
- async def test_bash_tool_restart_and_no_command():
37
- from hud.tools.types import ToolError
38
-
39
- tool = BashTool()
40
-
41
- from hud.tools.types import ContentResult
42
-
43
- class _FakeSession:
44
- async def run(self, cmd: str):
45
- return ContentResult(output="ran")
46
-
47
- async def start(self):
48
- return None
49
-
50
- def stop(self):
51
- return None
52
-
53
- tool.session = _FakeSession() # type: ignore[assignment]
54
-
55
- # Monkey-patch _BashSession.start to avoid launching a real shell
56
- async def _dummy_start(self):
57
- self._started = True
58
- from types import SimpleNamespace
59
-
60
- # minimal fake process attributes used later
61
- self._process = SimpleNamespace(returncode=None)
62
-
63
- import hud.tools.bash as bash_mod
64
-
65
- bash_mod._BashSession.start = _dummy_start # type: ignore[assignment]
66
-
67
- # restart=True returns system message
68
- res = await tool(command="ignored", restart=True)
69
- # Check that we get content blocks with the restart message
70
- assert len(res) > 0
71
- text_blocks = [b for b in res if isinstance(b, TextContent)]
72
- assert any("restarted" in b.text for b in text_blocks)
73
-
74
- # Calling without command raises ToolError
75
- with pytest.raises(ToolError):
76
- await tool()
77
-
78
-
79
- @pytest.mark.asyncio
80
- @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
81
- async def test_edit_tool_flow(tmp_path):
82
- file_path = tmp_path / "demo.txt"
83
-
84
- edit = EditTool()
85
-
86
- # create
87
- res = await edit(command="create", path=str(file_path), file_text="hello\nworld\n")
88
- # Check for success message in content blocks
89
- text_blocks = [b for b in res if isinstance(b, TextContent)]
90
- assert any("created" in b.text for b in text_blocks)
91
-
92
- # view
93
- res = await edit(command="view", path=str(file_path))
94
- # Check content blocks for file content
95
- text_blocks = [b for b in res if isinstance(b, TextContent)]
96
- combined_text = "".join(b.text for b in text_blocks)
97
- assert "hello" in combined_text
98
-
99
- # replace
100
- res = await edit(command="str_replace", path=str(file_path), old_str="world", new_str="earth")
101
- # Check for success message in content blocks
102
- text_blocks = [b for b in res if isinstance(b, TextContent)]
103
- combined_text = "".join(b.text for b in text_blocks)
104
- assert "has been edited" in combined_text
105
-
106
- # insert
107
- res = await edit(command="insert", path=str(file_path), insert_line=1, new_str="first line\n")
108
- assert res
109
-
110
-
111
- @pytest.mark.asyncio
112
- async def test_base_executor_simulation():
113
- from hud.tools.executors.base import BaseExecutor
114
-
115
- exec = BaseExecutor()
116
- res = await exec.execute("echo test")
117
- assert "SIMULATED" in (res.output or "")
118
- shot = await exec.screenshot()
119
- assert isinstance(shot, str) and len(shot) > 0
120
-
121
-
122
- @pytest.mark.asyncio
123
- @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
124
- async def test_edit_tool_view(tmp_path):
125
- # Create a temporary file
126
- p = tmp_path / "sample.txt"
127
- p.write_text("Sample content\n")
128
-
129
- tool = EditTool()
130
- result = await tool(command="view", path=str(p))
131
- # Check content blocks for file content
132
- text_blocks = [b for b in result if isinstance(b, TextContent)]
133
- combined_text = "".join(b.text for b in text_blocks)
134
- assert "Sample content" in combined_text
135
-
136
-
137
- @pytest.mark.asyncio
138
- async def test_computer_tool_screenshot():
139
- comp = HudComputerTool()
140
- blocks = await comp(action="screenshot")
141
- # Check that we got content blocks back
142
- assert blocks is not None
143
- assert len(blocks) > 0
144
- # Either ImageContent or TextContent is valid
145
- assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ import pytest
6
+ from mcp.types import ImageContent, TextContent
7
+
8
+ from hud.tools.bash import BashTool
9
+ from hud.tools.computer.hud import HudComputerTool
10
+ from hud.tools.edit import EditTool
11
+
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_bash_tool_echo():
15
+ tool = BashTool()
16
+
17
+ # Monkey-patch the private _session methods so no subprocess is spawned
18
+ from hud.tools.types import ContentResult
19
+
20
+ class _FakeSession:
21
+ async def run(self, cmd: str):
22
+ return ContentResult(output=f"mocked: {cmd}")
23
+
24
+ async def start(self):
25
+ return None
26
+
27
+ tool.session = _FakeSession() # type: ignore[assignment]
28
+
29
+ result = await tool(command="echo hello")
30
+ assert len(result) > 0
31
+ assert isinstance(result[0], TextContent)
32
+ assert result[0].text == "mocked: echo hello"
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_bash_tool_restart_and_no_command():
37
+ from hud.tools.types import ToolError
38
+
39
+ tool = BashTool()
40
+
41
+ from hud.tools.types import ContentResult
42
+
43
+ class _FakeSession:
44
+ async def run(self, cmd: str):
45
+ return ContentResult(output="ran")
46
+
47
+ async def start(self):
48
+ return None
49
+
50
+ def stop(self):
51
+ return None
52
+
53
+ tool.session = _FakeSession() # type: ignore[assignment]
54
+
55
+ # Monkey-patch _BashSession.start to avoid launching a real shell
56
+ async def _dummy_start(self):
57
+ self._started = True
58
+ from types import SimpleNamespace
59
+
60
+ # minimal fake process attributes used later
61
+ self._process = SimpleNamespace(returncode=None)
62
+
63
+ import hud.tools.bash as bash_mod
64
+
65
+ bash_mod._BashSession.start = _dummy_start # type: ignore[assignment]
66
+
67
+ # restart=True returns system message
68
+ res = await tool(command="ignored", restart=True)
69
+ # Check that we get content blocks with the restart message
70
+ assert len(res) > 0
71
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
72
+ assert any("restarted" in b.text for b in text_blocks)
73
+
74
+ # Calling without command raises ToolError
75
+ with pytest.raises(ToolError):
76
+ await tool()
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
81
+ async def test_edit_tool_flow(tmp_path):
82
+ file_path = tmp_path / "demo.txt"
83
+
84
+ edit = EditTool()
85
+
86
+ # create
87
+ res = await edit(command="create", path=str(file_path), file_text="hello\nworld\n")
88
+ # Check for success message in content blocks
89
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
90
+ assert any("created" in b.text for b in text_blocks)
91
+
92
+ # view
93
+ res = await edit(command="view", path=str(file_path))
94
+ # Check content blocks for file content
95
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
96
+ combined_text = "".join(b.text for b in text_blocks)
97
+ assert "hello" in combined_text
98
+
99
+ # replace
100
+ res = await edit(command="str_replace", path=str(file_path), old_str="world", new_str="earth")
101
+ # Check for success message in content blocks
102
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
103
+ combined_text = "".join(b.text for b in text_blocks)
104
+ assert "has been edited" in combined_text
105
+
106
+ # insert
107
+ res = await edit(command="insert", path=str(file_path), insert_line=1, new_str="first line\n")
108
+ assert res
109
+
110
+
111
+ @pytest.mark.asyncio
112
+ async def test_base_executor_simulation():
113
+ from hud.tools.executors.base import BaseExecutor
114
+
115
+ exec = BaseExecutor()
116
+ res = await exec.execute("echo test")
117
+ assert "SIMULATED" in (res.output or "")
118
+ shot = await exec.screenshot()
119
+ assert isinstance(shot, str) and len(shot) > 0
120
+
121
+
122
+ @pytest.mark.asyncio
123
+ @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
124
+ async def test_edit_tool_view(tmp_path):
125
+ # Create a temporary file
126
+ p = tmp_path / "sample.txt"
127
+ p.write_text("Sample content\n")
128
+
129
+ tool = EditTool()
130
+ result = await tool(command="view", path=str(p))
131
+ # Check content blocks for file content
132
+ text_blocks = [b for b in result if isinstance(b, TextContent)]
133
+ combined_text = "".join(b.text for b in text_blocks)
134
+ assert "Sample content" in combined_text
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_computer_tool_screenshot():
139
+ comp = HudComputerTool()
140
+ blocks = await comp(action="screenshot")
141
+ # Check that we got content blocks back
142
+ assert blocks is not None
143
+ assert len(blocks) > 0
144
+ # Either ImageContent or TextContent is valid
145
+ assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)