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.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {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
|
hud/tools/tests/test_tools.py
CHANGED
|
@@ -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)
|