hud-python 0.3.5__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +15 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +370 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +379 -0
  45. hud/clients/fastmcp.py +222 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.1.dist-info/METADATA +476 -0
  126. hud_python-0.4.1.dist-info/RECORD +132 -0
  127. hud_python-0.4.1.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +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_tool 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._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")
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._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._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