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,342 +1,342 @@
1
- """Tests for FastMCP client implementation."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import ANY, AsyncMock, MagicMock, patch
6
-
7
- import pytest
8
- from mcp import types
9
- from pydantic.networks import AnyUrl
10
-
11
- from hud.clients.fastmcp import FastMCPHUDClient
12
- from hud.types import MCPToolCall, MCPToolResult
13
-
14
-
15
- class TestFastMCPHUDClient:
16
- """Test FastMCP HUD client."""
17
-
18
- def test_initialization(self):
19
- """Test client initialization."""
20
- config = {"server1": {"command": "python", "args": ["server.py"]}}
21
-
22
- # Client is just instantiated, not connected yet
23
- client = FastMCPHUDClient(config)
24
-
25
- # Check that the client has the config stored
26
- assert client._mcp_config == config
27
- assert client._client is None # Not connected yet
28
-
29
- @pytest.mark.asyncio
30
- async def test_connect_creates_client(self):
31
- """Test that _connect creates the FastMCP client."""
32
- config = {"server1": {"command": "test"}}
33
-
34
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
35
- mock_fastmcp = AsyncMock()
36
- mock_client_class.return_value = mock_fastmcp
37
-
38
- client = FastMCPHUDClient(config)
39
- await client._connect(config)
40
-
41
- # Check FastMCP client was created
42
- mock_client_class.assert_called_once()
43
-
44
- # Check it was created with correct transport and client info
45
- call_args = mock_client_class.call_args
46
- assert call_args[0][0] == {"mcpServers": config}
47
- assert call_args[1]["client_info"].name == "hud-fastmcp"
48
-
49
- @pytest.mark.asyncio
50
- async def test_connect_logs_info(self):
51
- """Test that connect logs info message."""
52
- config = {"server1": {"command": "test"}}
53
-
54
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
55
- mock_fastmcp = AsyncMock()
56
- mock_client_class.return_value = mock_fastmcp
57
-
58
- client = FastMCPHUDClient(config)
59
-
60
- with patch("hud.clients.fastmcp.logger") as mock_logger:
61
- await client._connect(config)
62
-
63
- # Check info was logged
64
- mock_logger.info.assert_called_with("FastMCP client connected")
65
-
66
- @pytest.mark.asyncio
67
- async def test_list_tools(self):
68
- """Test listing tools."""
69
- config = {"server1": {"command": "test"}}
70
-
71
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
72
- mock_fastmcp = AsyncMock()
73
- mock_tools = [
74
- MagicMock(spec=types.Tool, name="tool1"),
75
- MagicMock(spec=types.Tool, name="tool2"),
76
- ]
77
- mock_fastmcp.list_tools.return_value = mock_tools
78
- mock_client_class.return_value = mock_fastmcp
79
-
80
- client = FastMCPHUDClient(config)
81
- client._initialized = True # Skip initialization
82
- client._client = mock_fastmcp # Set the mock client
83
-
84
- tools = await client.list_tools()
85
-
86
- assert tools == mock_tools
87
- mock_fastmcp.list_tools.assert_called_once()
88
-
89
- @pytest.mark.asyncio
90
- async def test_call_tool(self):
91
- """Test calling a tool."""
92
- config = {"server1": {"command": "test"}}
93
-
94
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
95
- mock_fastmcp = AsyncMock()
96
-
97
- # Mock FastMCP result
98
- mock_result = MagicMock()
99
- mock_result.content = [types.TextContent(type="text", text="result")]
100
- mock_result.is_error = False
101
- mock_result.structured_content = {"key": "value"}
102
-
103
- mock_fastmcp.call_tool.return_value = mock_result
104
- mock_client_class.return_value = mock_fastmcp
105
-
106
- client = FastMCPHUDClient(config)
107
- client._initialized = True
108
- client._client = mock_fastmcp # Set the mock client
109
-
110
- result = await client.call_tool(name="test_tool", arguments={"arg": "value"})
111
-
112
- assert isinstance(result, MCPToolResult)
113
- assert result.content == mock_result.content
114
- assert result.isError is False
115
- assert result.structuredContent == {"key": "value"}
116
-
117
- mock_fastmcp.call_tool.assert_called_once_with(
118
- name="test_tool",
119
- arguments={"arg": "value"},
120
- raise_on_error=False,
121
- )
122
-
123
- @pytest.mark.asyncio
124
- async def test_call_tool_with_mcp_tool_call(self):
125
- """Test calling a tool with MCPToolCall object."""
126
- config = {"server1": {"command": "test"}}
127
-
128
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
129
- mock_fastmcp = AsyncMock()
130
-
131
- # Mock FastMCP result
132
- mock_result = MagicMock()
133
- mock_result.content = [types.TextContent(type="text", text="result")]
134
- mock_result.is_error = False
135
- mock_result.structured_content = {"key": "value"}
136
-
137
- mock_fastmcp.call_tool.return_value = mock_result
138
- mock_client_class.return_value = mock_fastmcp
139
-
140
- client = FastMCPHUDClient(config)
141
- client._initialized = True
142
- client._client = mock_fastmcp # Set the mock client
143
-
144
- # Test with MCPToolCall object
145
- tool_call = MCPToolCall(name="test_tool", arguments={"arg": "value"})
146
- result = await client.call_tool(tool_call)
147
-
148
- assert isinstance(result, MCPToolResult)
149
- assert result.content == mock_result.content
150
- assert result.isError is False
151
- assert result.structuredContent == {"key": "value"}
152
-
153
- mock_fastmcp.call_tool.assert_called_once_with(
154
- name="test_tool",
155
- arguments={"arg": "value"},
156
- raise_on_error=False,
157
- )
158
-
159
- @pytest.mark.asyncio
160
- async def test_call_tool_no_arguments(self):
161
- """Test calling a tool without arguments."""
162
- config = {"server1": {"command": "test"}}
163
-
164
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
165
- mock_fastmcp = AsyncMock()
166
- mock_result = MagicMock()
167
- mock_result.content = []
168
- mock_result.is_error = True
169
- mock_result.structured_content = None
170
-
171
- mock_fastmcp.call_tool.return_value = mock_result
172
- mock_client_class.return_value = mock_fastmcp
173
-
174
- client = FastMCPHUDClient(config)
175
- client._initialized = True
176
- client._client = mock_fastmcp # Set the mock client
177
-
178
- await client.call_tool(name="test_tool", arguments={})
179
-
180
- # Should pass empty dict for arguments
181
- mock_fastmcp.call_tool.assert_called_once_with(
182
- name="test_tool",
183
- arguments={},
184
- raise_on_error=False,
185
- )
186
-
187
- @pytest.mark.asyncio
188
- async def test_list_resources(self):
189
- """Test listing resources."""
190
- config = {"server1": {"command": "test"}}
191
-
192
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
193
- mock_fastmcp = AsyncMock()
194
- mock_resources = [
195
- MagicMock(spec=types.Resource, uri="file:///test1"),
196
- MagicMock(spec=types.Resource, uri="file:///test2"),
197
- ]
198
- mock_fastmcp.list_resources.return_value = mock_resources
199
- mock_client_class.return_value = mock_fastmcp
200
-
201
- client = FastMCPHUDClient(config)
202
- client._initialized = True
203
- client._client = mock_fastmcp # Set the mock client
204
-
205
- resources = await client.list_resources()
206
-
207
- assert resources == mock_resources
208
- mock_fastmcp.list_resources.assert_called_once()
209
-
210
- @pytest.mark.asyncio
211
- async def test_read_resource_internal_success(self):
212
- """Test reading a resource successfully."""
213
- config = {"server1": {"command": "test"}}
214
-
215
- # Create proper resource contents that ReadResourceResult expects
216
- mock_contents = [
217
- types.TextResourceContents(
218
- uri=AnyUrl("file:///test"),
219
- mimeType="text/plain",
220
- text="resource content",
221
- )
222
- ]
223
-
224
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
225
- # Create a mock FastMCP client
226
- mock_fastmcp = AsyncMock()
227
- mock_fastmcp.read_resource.return_value = mock_contents
228
- mock_client_class.return_value = mock_fastmcp
229
-
230
- # Now create the HUD client - it will use our mocked FastMCP client
231
- client = FastMCPHUDClient(config)
232
- client._initialized = True
233
- client._client = mock_fastmcp # Set the mock client
234
-
235
- result = await client.read_resource("file:///test")
236
-
237
- assert isinstance(result, types.ReadResourceResult)
238
- assert result.contents == mock_contents
239
- mock_fastmcp.read_resource.assert_called_once_with("file:///test")
240
-
241
- @pytest.mark.asyncio
242
- async def test_read_resource_internal_error_verbose(self):
243
- """Test reading a resource with error in verbose mode."""
244
- config = {"server1": {"command": "test"}}
245
-
246
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
247
- mock_fastmcp = AsyncMock()
248
- mock_fastmcp.read_resource.side_effect = Exception("Read failed")
249
- mock_client_class.return_value = mock_fastmcp
250
-
251
- client = FastMCPHUDClient(config, verbose=True)
252
- client._initialized = True
253
- client._client = mock_fastmcp # Set the mock client
254
-
255
- with patch("hud.clients.fastmcp.logger") as mock_logger:
256
- result = await client.read_resource("file:///bad")
257
-
258
- assert result is None
259
- mock_logger.warning.assert_called_with(
260
- "Unexpected error reading resource '%s': %s", "file:///bad", ANY
261
- )
262
-
263
- @pytest.mark.asyncio
264
- async def test_read_resource_internal_error_not_verbose(self):
265
- """Test reading a resource with error in non-verbose mode."""
266
- config = {"server1": {"command": "test"}}
267
-
268
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
269
- mock_fastmcp = AsyncMock()
270
- mock_fastmcp.read_resource.side_effect = Exception("Read failed")
271
- mock_client_class.return_value = mock_fastmcp
272
-
273
- client = FastMCPHUDClient(config, verbose=False)
274
- client._initialized = True
275
- client._client = mock_fastmcp # Set the mock client
276
-
277
- with patch("hud.clients.fastmcp.logger") as mock_logger:
278
- result = await client.read_resource("file:///bad")
279
-
280
- assert result is None
281
- # Should not log in non-verbose mode
282
- mock_logger.debug.assert_not_called()
283
-
284
- @pytest.mark.asyncio
285
- async def test_shutdown(self):
286
- """Test shutting down the client."""
287
- config = {"server1": {"command": "test"}}
288
-
289
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
290
- mock_fastmcp = AsyncMock()
291
- mock_client_class.return_value = mock_fastmcp
292
-
293
- client = FastMCPHUDClient(config)
294
-
295
- # Set up stack and client
296
- mock_stack = AsyncMock()
297
- client._stack = mock_stack
298
- client._initialized = True
299
- client._client = mock_fastmcp # Set the mock client
300
-
301
- with patch("hud.clients.fastmcp.logger") as mock_logger:
302
- await client.shutdown()
303
-
304
- mock_stack.aclose.assert_called_once()
305
- assert client._stack is None
306
- assert client._initialized is False
307
- mock_logger.debug.assert_called_with("FastMCP client closed")
308
-
309
- @pytest.mark.asyncio
310
- async def test_shutdown_no_stack(self):
311
- """Test shutting down when no stack exists."""
312
- config = {"server1": {"command": "test"}}
313
-
314
- with patch("hud.clients.fastmcp.FastMCPClient"):
315
- client = FastMCPHUDClient(config)
316
- client._stack = None
317
-
318
- # Should not raise error
319
- await client.shutdown()
320
-
321
- assert client._stack is None
322
-
323
- @pytest.mark.asyncio
324
- async def test_context_manager(self):
325
- """Test using client as async context manager."""
326
- config = {"server1": {"command": "test"}}
327
-
328
- with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
329
- mock_fastmcp = AsyncMock()
330
- mock_client_class.return_value = mock_fastmcp
331
-
332
- client = FastMCPHUDClient(config)
333
-
334
- with (
335
- patch.object(client, "initialize", new_callable=AsyncMock) as mock_init,
336
- patch.object(client, "shutdown", new_callable=AsyncMock) as mock_close,
337
- ):
338
- async with client as ctx:
339
- assert ctx is client
340
- mock_init.assert_called_once()
341
-
342
- mock_close.assert_called_once()
1
+ """Tests for FastMCP client implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import ANY, AsyncMock, MagicMock, patch
6
+
7
+ import pytest
8
+ from mcp import types
9
+ from pydantic.networks import AnyUrl
10
+
11
+ from hud.clients.fastmcp import FastMCPHUDClient
12
+ from hud.types import MCPToolCall, MCPToolResult
13
+
14
+
15
+ class TestFastMCPHUDClient:
16
+ """Test FastMCP HUD client."""
17
+
18
+ def test_initialization(self):
19
+ """Test client initialization."""
20
+ config = {"server1": {"command": "python", "args": ["server.py"]}}
21
+
22
+ # Client is just instantiated, not connected yet
23
+ client = FastMCPHUDClient(config)
24
+
25
+ # Check that the client has the config stored
26
+ assert client._mcp_config == config
27
+ assert client._client is None # Not connected yet
28
+
29
+ @pytest.mark.asyncio
30
+ async def test_connect_creates_client(self):
31
+ """Test that _connect creates the FastMCP client."""
32
+ config = {"server1": {"command": "test"}}
33
+
34
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
35
+ mock_fastmcp = AsyncMock()
36
+ mock_client_class.return_value = mock_fastmcp
37
+
38
+ client = FastMCPHUDClient(config)
39
+ await client._connect(config)
40
+
41
+ # Check FastMCP client was created
42
+ mock_client_class.assert_called_once()
43
+
44
+ # Check it was created with correct transport and client info
45
+ call_args = mock_client_class.call_args
46
+ assert call_args[0][0] == {"mcpServers": config}
47
+ assert call_args[1]["client_info"].name == "hud-fastmcp"
48
+
49
+ @pytest.mark.asyncio
50
+ async def test_connect_logs_info(self):
51
+ """Test that connect logs info message."""
52
+ config = {"server1": {"command": "test"}}
53
+
54
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
55
+ mock_fastmcp = AsyncMock()
56
+ mock_client_class.return_value = mock_fastmcp
57
+
58
+ client = FastMCPHUDClient(config)
59
+
60
+ with patch("hud.clients.fastmcp.logger") as mock_logger:
61
+ await client._connect(config)
62
+
63
+ # Check info was logged
64
+ mock_logger.info.assert_called_with("FastMCP client connected")
65
+
66
+ @pytest.mark.asyncio
67
+ async def test_list_tools(self):
68
+ """Test listing tools."""
69
+ config = {"server1": {"command": "test"}}
70
+
71
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
72
+ mock_fastmcp = AsyncMock()
73
+ mock_tools = [
74
+ MagicMock(spec=types.Tool, name="tool1"),
75
+ MagicMock(spec=types.Tool, name="tool2"),
76
+ ]
77
+ mock_fastmcp.list_tools.return_value = mock_tools
78
+ mock_client_class.return_value = mock_fastmcp
79
+
80
+ client = FastMCPHUDClient(config)
81
+ client._initialized = True # Skip initialization
82
+ client._client = mock_fastmcp # Set the mock client
83
+
84
+ tools = await client.list_tools()
85
+
86
+ assert tools == mock_tools
87
+ mock_fastmcp.list_tools.assert_called_once()
88
+
89
+ @pytest.mark.asyncio
90
+ async def test_call_tool(self):
91
+ """Test calling a tool."""
92
+ config = {"server1": {"command": "test"}}
93
+
94
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
95
+ mock_fastmcp = AsyncMock()
96
+
97
+ # Mock FastMCP result
98
+ mock_result = MagicMock()
99
+ mock_result.content = [types.TextContent(type="text", text="result")]
100
+ mock_result.is_error = False
101
+ mock_result.structured_content = {"key": "value"}
102
+
103
+ mock_fastmcp.call_tool.return_value = mock_result
104
+ mock_client_class.return_value = mock_fastmcp
105
+
106
+ client = FastMCPHUDClient(config)
107
+ client._initialized = True
108
+ client._client = mock_fastmcp # Set the mock client
109
+
110
+ result = await client.call_tool(name="test_tool", arguments={"arg": "value"})
111
+
112
+ assert isinstance(result, MCPToolResult)
113
+ assert result.content == mock_result.content
114
+ assert result.isError is False
115
+ assert result.structuredContent == {"key": "value"}
116
+
117
+ mock_fastmcp.call_tool.assert_called_once_with(
118
+ name="test_tool",
119
+ arguments={"arg": "value"},
120
+ raise_on_error=False,
121
+ )
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_call_tool_with_mcp_tool_call(self):
125
+ """Test calling a tool with MCPToolCall object."""
126
+ config = {"server1": {"command": "test"}}
127
+
128
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
129
+ mock_fastmcp = AsyncMock()
130
+
131
+ # Mock FastMCP result
132
+ mock_result = MagicMock()
133
+ mock_result.content = [types.TextContent(type="text", text="result")]
134
+ mock_result.is_error = False
135
+ mock_result.structured_content = {"key": "value"}
136
+
137
+ mock_fastmcp.call_tool.return_value = mock_result
138
+ mock_client_class.return_value = mock_fastmcp
139
+
140
+ client = FastMCPHUDClient(config)
141
+ client._initialized = True
142
+ client._client = mock_fastmcp # Set the mock client
143
+
144
+ # Test with MCPToolCall object
145
+ tool_call = MCPToolCall(name="test_tool", arguments={"arg": "value"})
146
+ result = await client.call_tool(tool_call)
147
+
148
+ assert isinstance(result, MCPToolResult)
149
+ assert result.content == mock_result.content
150
+ assert result.isError is False
151
+ assert result.structuredContent == {"key": "value"}
152
+
153
+ mock_fastmcp.call_tool.assert_called_once_with(
154
+ name="test_tool",
155
+ arguments={"arg": "value"},
156
+ raise_on_error=False,
157
+ )
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_call_tool_no_arguments(self):
161
+ """Test calling a tool without arguments."""
162
+ config = {"server1": {"command": "test"}}
163
+
164
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
165
+ mock_fastmcp = AsyncMock()
166
+ mock_result = MagicMock()
167
+ mock_result.content = []
168
+ mock_result.is_error = True
169
+ mock_result.structured_content = None
170
+
171
+ mock_fastmcp.call_tool.return_value = mock_result
172
+ mock_client_class.return_value = mock_fastmcp
173
+
174
+ client = FastMCPHUDClient(config)
175
+ client._initialized = True
176
+ client._client = mock_fastmcp # Set the mock client
177
+
178
+ await client.call_tool(name="test_tool", arguments={})
179
+
180
+ # Should pass empty dict for arguments
181
+ mock_fastmcp.call_tool.assert_called_once_with(
182
+ name="test_tool",
183
+ arguments={},
184
+ raise_on_error=False,
185
+ )
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_list_resources(self):
189
+ """Test listing resources."""
190
+ config = {"server1": {"command": "test"}}
191
+
192
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
193
+ mock_fastmcp = AsyncMock()
194
+ mock_resources = [
195
+ MagicMock(spec=types.Resource, uri="file:///test1"),
196
+ MagicMock(spec=types.Resource, uri="file:///test2"),
197
+ ]
198
+ mock_fastmcp.list_resources.return_value = mock_resources
199
+ mock_client_class.return_value = mock_fastmcp
200
+
201
+ client = FastMCPHUDClient(config)
202
+ client._initialized = True
203
+ client._client = mock_fastmcp # Set the mock client
204
+
205
+ resources = await client.list_resources()
206
+
207
+ assert resources == mock_resources
208
+ mock_fastmcp.list_resources.assert_called_once()
209
+
210
+ @pytest.mark.asyncio
211
+ async def test_read_resource_internal_success(self):
212
+ """Test reading a resource successfully."""
213
+ config = {"server1": {"command": "test"}}
214
+
215
+ # Create proper resource contents that ReadResourceResult expects
216
+ mock_contents = [
217
+ types.TextResourceContents(
218
+ uri=AnyUrl("file:///test"),
219
+ mimeType="text/plain",
220
+ text="resource content",
221
+ )
222
+ ]
223
+
224
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
225
+ # Create a mock FastMCP client
226
+ mock_fastmcp = AsyncMock()
227
+ mock_fastmcp.read_resource.return_value = mock_contents
228
+ mock_client_class.return_value = mock_fastmcp
229
+
230
+ # Now create the HUD client - it will use our mocked FastMCP client
231
+ client = FastMCPHUDClient(config)
232
+ client._initialized = True
233
+ client._client = mock_fastmcp # Set the mock client
234
+
235
+ result = await client.read_resource("file:///test")
236
+
237
+ assert isinstance(result, types.ReadResourceResult)
238
+ assert result.contents == mock_contents
239
+ mock_fastmcp.read_resource.assert_called_once_with("file:///test")
240
+
241
+ @pytest.mark.asyncio
242
+ async def test_read_resource_internal_error_verbose(self):
243
+ """Test reading a resource with error in verbose mode."""
244
+ config = {"server1": {"command": "test"}}
245
+
246
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
247
+ mock_fastmcp = AsyncMock()
248
+ mock_fastmcp.read_resource.side_effect = Exception("Read failed")
249
+ mock_client_class.return_value = mock_fastmcp
250
+
251
+ client = FastMCPHUDClient(config, verbose=True)
252
+ client._initialized = True
253
+ client._client = mock_fastmcp # Set the mock client
254
+
255
+ with patch("hud.clients.fastmcp.logger") as mock_logger:
256
+ result = await client.read_resource("file:///bad")
257
+
258
+ assert result is None
259
+ mock_logger.warning.assert_called_with(
260
+ "Unexpected error reading resource '%s': %s", "file:///bad", ANY
261
+ )
262
+
263
+ @pytest.mark.asyncio
264
+ async def test_read_resource_internal_error_not_verbose(self):
265
+ """Test reading a resource with error in non-verbose mode."""
266
+ config = {"server1": {"command": "test"}}
267
+
268
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
269
+ mock_fastmcp = AsyncMock()
270
+ mock_fastmcp.read_resource.side_effect = Exception("Read failed")
271
+ mock_client_class.return_value = mock_fastmcp
272
+
273
+ client = FastMCPHUDClient(config, verbose=False)
274
+ client._initialized = True
275
+ client._client = mock_fastmcp # Set the mock client
276
+
277
+ with patch("hud.clients.fastmcp.logger") as mock_logger:
278
+ result = await client.read_resource("file:///bad")
279
+
280
+ assert result is None
281
+ # Should not log in non-verbose mode
282
+ mock_logger.debug.assert_not_called()
283
+
284
+ @pytest.mark.asyncio
285
+ async def test_shutdown(self):
286
+ """Test shutting down the client."""
287
+ config = {"server1": {"command": "test"}}
288
+
289
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
290
+ mock_fastmcp = AsyncMock()
291
+ mock_client_class.return_value = mock_fastmcp
292
+
293
+ client = FastMCPHUDClient(config)
294
+
295
+ # Set up stack and client
296
+ mock_stack = AsyncMock()
297
+ client._stack = mock_stack
298
+ client._initialized = True
299
+ client._client = mock_fastmcp # Set the mock client
300
+
301
+ with patch("hud.clients.fastmcp.logger") as mock_logger:
302
+ await client.shutdown()
303
+
304
+ mock_stack.aclose.assert_called_once()
305
+ assert client._stack is None
306
+ assert client._initialized is False
307
+ mock_logger.debug.assert_called_with("FastMCP client closed")
308
+
309
+ @pytest.mark.asyncio
310
+ async def test_shutdown_no_stack(self):
311
+ """Test shutting down when no stack exists."""
312
+ config = {"server1": {"command": "test"}}
313
+
314
+ with patch("hud.clients.fastmcp.FastMCPClient"):
315
+ client = FastMCPHUDClient(config)
316
+ client._stack = None
317
+
318
+ # Should not raise error
319
+ await client.shutdown()
320
+
321
+ assert client._stack is None
322
+
323
+ @pytest.mark.asyncio
324
+ async def test_context_manager(self):
325
+ """Test using client as async context manager."""
326
+ config = {"server1": {"command": "test"}}
327
+
328
+ with patch("hud.clients.fastmcp.FastMCPClient") as mock_client_class:
329
+ mock_fastmcp = AsyncMock()
330
+ mock_client_class.return_value = mock_fastmcp
331
+
332
+ client = FastMCPHUDClient(config)
333
+
334
+ with (
335
+ patch.object(client, "initialize", new_callable=AsyncMock) as mock_init,
336
+ patch.object(client, "shutdown", new_callable=AsyncMock) as mock_close,
337
+ ):
338
+ async with client as ctx:
339
+ assert ctx is client
340
+ mock_init.assert_called_once()
341
+
342
+ mock_close.assert_called_once()