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,284 +1,284 @@
1
- """Tests for hud.cli.analyze module."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from pathlib import Path
7
- from unittest.mock import AsyncMock, MagicMock, mock_open, patch
8
-
9
- import pytest
10
-
11
- from hud.cli.analyze import (
12
- _analyze_with_config,
13
- analyze_environment,
14
- analyze_environment_from_config,
15
- analyze_environment_from_mcp_config,
16
- display_interactive,
17
- display_markdown,
18
- parse_docker_command,
19
- )
20
-
21
-
22
- class TestParseDockerCommand:
23
- """Test Docker command parsing."""
24
-
25
- def test_parse_simple_docker_command(self) -> None:
26
- """Test parsing simple Docker command."""
27
- docker_cmd = ["docker", "run", "image:latest"]
28
- result = parse_docker_command(docker_cmd)
29
- assert result == {"local": {"command": "docker", "args": ["run", "image:latest"]}}
30
-
31
- def test_parse_docker_command_no_args(self) -> None:
32
- """Test parsing Docker command with no arguments."""
33
- docker_cmd = ["docker"]
34
- result = parse_docker_command(docker_cmd)
35
- assert result == {"local": {"command": "docker", "args": []}}
36
-
37
-
38
- class TestAnalyzeEnvironment:
39
- """Test main analyze_environment function."""
40
-
41
- @pytest.mark.asyncio
42
- async def test_analyze_environment_success(self) -> None:
43
- """Test successful environment analysis."""
44
- mock_analysis = {
45
- "metadata": {"servers": ["test"], "initialized": True},
46
- "tools": [{"name": "tool1", "description": "Test tool"}],
47
- "hub_tools": {},
48
- "resources": [],
49
- "telemetry": {},
50
- }
51
-
52
- with (
53
- patch("hud.cli.analyze.MCPClient") as MockClient,
54
- patch("hud.cli.analyze.console"),
55
- patch("hud.cli.analyze.display_interactive") as mock_interactive,
56
- ):
57
- # Setup mock client - return an instance with async methods
58
- mock_client = MagicMock()
59
- mock_client.initialize = AsyncMock()
60
- mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
61
- mock_client.shutdown = AsyncMock()
62
- MockClient.return_value = mock_client
63
-
64
- await analyze_environment(
65
- ["docker", "run", "test"],
66
- output_format="interactive",
67
- verbose=False,
68
- )
69
-
70
- # Check client was used correctly
71
- MockClient.assert_called_once()
72
- mock_client.initialize.assert_called_once()
73
- mock_client.analyze_environment.assert_called_once()
74
- mock_client.shutdown.assert_called_once()
75
-
76
- # Check interactive display was called
77
- mock_interactive.assert_called_once_with(mock_analysis)
78
-
79
- @pytest.mark.asyncio
80
- async def test_analyze_environment_failure(self) -> None:
81
- """Test handling analysis failure."""
82
- with (
83
- patch("hud.cli.analyze.MCPClient") as MockClient,
84
- patch("hud.cli.analyze.console") as mock_console,
85
- ):
86
- # Setup mock client that will raise exception during initialization
87
- mock_client = MagicMock()
88
- mock_client.initialize = AsyncMock(side_effect=RuntimeError("Connection failed"))
89
- mock_client.shutdown = AsyncMock()
90
- MockClient.return_value = mock_client
91
-
92
- # Test should not raise exception
93
- await analyze_environment(
94
- ["docker", "run", "test"],
95
- output_format="json",
96
- verbose=False,
97
- )
98
-
99
- # Check error was handled
100
- mock_client.initialize.assert_called_once()
101
- mock_client.shutdown.assert_called_once()
102
-
103
- # Check console printed error hints
104
- calls = mock_console.print.call_args_list
105
- assert any("Docker logs may not show on Windows" in str(call) for call in calls)
106
-
107
- @pytest.mark.asyncio
108
- async def test_analyze_environment_formats(self) -> None:
109
- """Test different output formats."""
110
- mock_analysis = {
111
- "metadata": {"servers": ["test"], "initialized": True},
112
- "tools": [],
113
- "hub_tools": {},
114
- "resources": [],
115
- "telemetry": {},
116
- "verbose": False,
117
- }
118
-
119
- for output_format in ["json", "markdown", "interactive"]:
120
- with (
121
- patch("hud.cli.analyze.MCPClient") as MockClient,
122
- patch("hud.cli.analyze.console") as mock_console,
123
- patch("hud.cli.analyze.display_interactive") as mock_interactive,
124
- patch("hud.cli.analyze.display_markdown") as mock_markdown,
125
- ):
126
- # Setup mock client
127
- mock_client = MagicMock()
128
- mock_client.initialize = AsyncMock()
129
- mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
130
- mock_client.shutdown = AsyncMock()
131
- MockClient.return_value = mock_client
132
-
133
- # Run analysis
134
- await analyze_environment(
135
- ["docker", "run", "test"],
136
- output_format=output_format,
137
- verbose=False,
138
- )
139
-
140
- # Check correct display function was called
141
- if output_format == "json":
142
- mock_console.print_json.assert_called()
143
- elif output_format == "markdown":
144
- mock_markdown.assert_called_once_with(mock_analysis)
145
- else: # interactive
146
- mock_interactive.assert_called_once_with(mock_analysis)
147
-
148
-
149
- class TestAnalyzeWithConfig:
150
- """Test config-based analysis functions."""
151
-
152
- @pytest.mark.asyncio
153
- async def test_analyze_with_config_success(self) -> None:
154
- """Test successful config-based analysis."""
155
- mock_config = {"server": {"command": "test", "args": ["--arg"]}}
156
- mock_analysis = {
157
- "metadata": {"servers": ["server"], "initialized": True},
158
- "tools": [],
159
- "hub_tools": {},
160
- "resources": [],
161
- "telemetry": {},
162
- }
163
-
164
- with (
165
- patch("hud.cli.analyze.MCPClient") as MockClient,
166
- patch("hud.cli.analyze.console"),
167
- patch("hud.cli.analyze.display_interactive") as mock_interactive,
168
- ):
169
- # Setup mock client
170
- mock_client = MagicMock()
171
- mock_client.initialize = AsyncMock()
172
- mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
173
- mock_client.shutdown = AsyncMock()
174
- MockClient.return_value = mock_client
175
-
176
- await _analyze_with_config(
177
- mock_config,
178
- output_format="interactive",
179
- verbose=False,
180
- )
181
-
182
- # Check client was created with correct config
183
- MockClient.assert_called_once_with(mcp_config=mock_config, verbose=False)
184
- mock_interactive.assert_called_once_with(mock_analysis)
185
-
186
- @pytest.mark.asyncio
187
- async def test_analyze_with_config_exception(self) -> None:
188
- """Test config analysis handles exceptions gracefully."""
189
- mock_config = {"server": {"command": "test"}}
190
-
191
- with (
192
- patch("hud.cli.analyze.MCPClient") as MockClient,
193
- patch("hud.cli.analyze.console"),
194
- ):
195
- # Setup mock client that fails
196
- mock_client = MagicMock()
197
- mock_client.initialize = AsyncMock(side_effect=Exception("Test error"))
198
- mock_client.shutdown = AsyncMock()
199
- MockClient.return_value = mock_client
200
-
201
- # Should not raise
202
- await _analyze_with_config(
203
- mock_config,
204
- output_format="json",
205
- verbose=False,
206
- )
207
-
208
- mock_client.shutdown.assert_called_once()
209
-
210
- @pytest.mark.asyncio
211
- async def test_analyze_environment_from_config(self) -> None:
212
- """Test analyze_environment_from_config."""
213
- config_data = {"server": {"command": "test"}}
214
- mock_path = Path("test.json")
215
-
216
- with (
217
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
218
- patch("hud.cli.analyze._analyze_with_config") as mock_analyze,
219
- ):
220
- await analyze_environment_from_config(mock_path, "json", False)
221
-
222
- mock_analyze.assert_called_once_with(config_data, "json", False)
223
-
224
- @pytest.mark.asyncio
225
- async def test_analyze_environment_from_mcp_config(self) -> None:
226
- """Test analyze_environment_from_mcp_config."""
227
- config_data = {"server": {"command": "test"}}
228
-
229
- with patch("hud.cli.analyze._analyze_with_config") as mock_analyze:
230
- await analyze_environment_from_mcp_config(config_data, "markdown", True)
231
-
232
- mock_analyze.assert_called_once_with(config_data, "markdown", True)
233
-
234
-
235
- class TestDisplayFunctions:
236
- """Test display formatting functions."""
237
-
238
- def test_display_interactive_basic(self) -> None:
239
- """Test basic interactive display."""
240
- analysis = {
241
- "metadata": {"servers": ["test"], "initialized": True},
242
- "tools": [{"name": "tool1", "description": "Test tool"}],
243
- "hub_tools": {"hub1": ["func1", "func2"]},
244
- "resources": [{"uri": "file:///test", "name": "Test", "description": "Resource"}],
245
- "telemetry": {"status": "running", "live_url": "http://test"},
246
- }
247
-
248
- with patch("hud.cli.analyze.console") as mock_console:
249
- display_interactive(analysis)
250
-
251
- # Check console was called multiple times
252
- assert mock_console.print.call_count > 0
253
- # The design.section_title uses its own console, not the patched one
254
- # Just verify the function ran without errors
255
-
256
- def test_display_markdown_basic(self) -> None:
257
- """Test basic markdown display."""
258
- analysis = {
259
- "metadata": {"servers": ["test1", "test2"], "initialized": True},
260
- "tools": [
261
- {"name": "tool1", "description": "Tool 1"},
262
- {"name": "setup", "description": "Hub tool"},
263
- ],
264
- "hub_tools": {"setup": ["init", "config"]},
265
- "resources": [{"uri": "telemetry://live", "name": "Telemetry"}],
266
- "telemetry": {"status": "active"},
267
- }
268
-
269
- with patch("hud.cli.analyze.console") as mock_console:
270
- display_markdown(analysis)
271
-
272
- # Get the markdown output
273
- mock_console.print.assert_called_once()
274
- markdown = mock_console.print.call_args[0][0]
275
-
276
- # Check markdown structure
277
- assert "# MCP Environment Analysis" in markdown
278
- assert "## Environment Overview" in markdown
279
- assert "## Available Tools" in markdown
280
- assert "### Regular Tools" in markdown
281
- assert "### Hub Tools" in markdown
282
- assert "- **tool1**: Tool 1" in markdown
283
- assert "- **setup**" in markdown
284
- assert " - init" in markdown
1
+ """Tests for hud.cli.analyze module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import AsyncMock, MagicMock, mock_open, patch
8
+
9
+ import pytest
10
+
11
+ from hud.cli.analyze import (
12
+ _analyze_with_config,
13
+ analyze_environment,
14
+ analyze_environment_from_config,
15
+ analyze_environment_from_mcp_config,
16
+ display_interactive,
17
+ display_markdown,
18
+ parse_docker_command,
19
+ )
20
+
21
+
22
+ class TestParseDockerCommand:
23
+ """Test Docker command parsing."""
24
+
25
+ def test_parse_simple_docker_command(self) -> None:
26
+ """Test parsing simple Docker command."""
27
+ docker_cmd = ["docker", "run", "image:latest"]
28
+ result = parse_docker_command(docker_cmd)
29
+ assert result == {"local": {"command": "docker", "args": ["run", "image:latest"]}}
30
+
31
+ def test_parse_docker_command_no_args(self) -> None:
32
+ """Test parsing Docker command with no arguments."""
33
+ docker_cmd = ["docker"]
34
+ result = parse_docker_command(docker_cmd)
35
+ assert result == {"local": {"command": "docker", "args": []}}
36
+
37
+
38
+ class TestAnalyzeEnvironment:
39
+ """Test main analyze_environment function."""
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_analyze_environment_success(self) -> None:
43
+ """Test successful environment analysis."""
44
+ mock_analysis = {
45
+ "metadata": {"servers": ["test"], "initialized": True},
46
+ "tools": [{"name": "tool1", "description": "Test tool"}],
47
+ "hub_tools": {},
48
+ "resources": [],
49
+ "telemetry": {},
50
+ }
51
+
52
+ with (
53
+ patch("hud.cli.analyze.MCPClient") as MockClient,
54
+ patch("hud.cli.analyze.console"),
55
+ patch("hud.cli.analyze.display_interactive") as mock_interactive,
56
+ ):
57
+ # Setup mock client - return an instance with async methods
58
+ mock_client = MagicMock()
59
+ mock_client.initialize = AsyncMock()
60
+ mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
61
+ mock_client.shutdown = AsyncMock()
62
+ MockClient.return_value = mock_client
63
+
64
+ await analyze_environment(
65
+ ["docker", "run", "test"],
66
+ output_format="interactive",
67
+ verbose=False,
68
+ )
69
+
70
+ # Check client was used correctly
71
+ MockClient.assert_called_once()
72
+ mock_client.initialize.assert_called_once()
73
+ mock_client.analyze_environment.assert_called_once()
74
+ mock_client.shutdown.assert_called_once()
75
+
76
+ # Check interactive display was called
77
+ mock_interactive.assert_called_once_with(mock_analysis)
78
+
79
+ @pytest.mark.asyncio
80
+ async def test_analyze_environment_failure(self) -> None:
81
+ """Test handling analysis failure."""
82
+ with (
83
+ patch("hud.cli.analyze.MCPClient") as MockClient,
84
+ patch("hud.cli.analyze.console") as mock_console,
85
+ ):
86
+ # Setup mock client that will raise exception during initialization
87
+ mock_client = MagicMock()
88
+ mock_client.initialize = AsyncMock(side_effect=RuntimeError("Connection failed"))
89
+ mock_client.shutdown = AsyncMock()
90
+ MockClient.return_value = mock_client
91
+
92
+ # Test should not raise exception
93
+ await analyze_environment(
94
+ ["docker", "run", "test"],
95
+ output_format="json",
96
+ verbose=False,
97
+ )
98
+
99
+ # Check error was handled
100
+ mock_client.initialize.assert_called_once()
101
+ mock_client.shutdown.assert_called_once()
102
+
103
+ # Check console printed error hints
104
+ calls = mock_console.print.call_args_list
105
+ assert any("Docker logs may not show on Windows" in str(call) for call in calls)
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_analyze_environment_formats(self) -> None:
109
+ """Test different output formats."""
110
+ mock_analysis = {
111
+ "metadata": {"servers": ["test"], "initialized": True},
112
+ "tools": [],
113
+ "hub_tools": {},
114
+ "resources": [],
115
+ "telemetry": {},
116
+ "verbose": False,
117
+ }
118
+
119
+ for output_format in ["json", "markdown", "interactive"]:
120
+ with (
121
+ patch("hud.cli.analyze.MCPClient") as MockClient,
122
+ patch("hud.cli.analyze.console") as mock_console,
123
+ patch("hud.cli.analyze.display_interactive") as mock_interactive,
124
+ patch("hud.cli.analyze.display_markdown") as mock_markdown,
125
+ ):
126
+ # Setup mock client
127
+ mock_client = MagicMock()
128
+ mock_client.initialize = AsyncMock()
129
+ mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
130
+ mock_client.shutdown = AsyncMock()
131
+ MockClient.return_value = mock_client
132
+
133
+ # Run analysis
134
+ await analyze_environment(
135
+ ["docker", "run", "test"],
136
+ output_format=output_format,
137
+ verbose=False,
138
+ )
139
+
140
+ # Check correct display function was called
141
+ if output_format == "json":
142
+ mock_console.print_json.assert_called()
143
+ elif output_format == "markdown":
144
+ mock_markdown.assert_called_once_with(mock_analysis)
145
+ else: # interactive
146
+ mock_interactive.assert_called_once_with(mock_analysis)
147
+
148
+
149
+ class TestAnalyzeWithConfig:
150
+ """Test config-based analysis functions."""
151
+
152
+ @pytest.mark.asyncio
153
+ async def test_analyze_with_config_success(self) -> None:
154
+ """Test successful config-based analysis."""
155
+ mock_config = {"server": {"command": "test", "args": ["--arg"]}}
156
+ mock_analysis = {
157
+ "metadata": {"servers": ["server"], "initialized": True},
158
+ "tools": [],
159
+ "hub_tools": {},
160
+ "resources": [],
161
+ "telemetry": {},
162
+ }
163
+
164
+ with (
165
+ patch("hud.cli.analyze.MCPClient") as MockClient,
166
+ patch("hud.cli.analyze.console"),
167
+ patch("hud.cli.analyze.display_interactive") as mock_interactive,
168
+ ):
169
+ # Setup mock client
170
+ mock_client = MagicMock()
171
+ mock_client.initialize = AsyncMock()
172
+ mock_client.analyze_environment = AsyncMock(return_value=mock_analysis)
173
+ mock_client.shutdown = AsyncMock()
174
+ MockClient.return_value = mock_client
175
+
176
+ await _analyze_with_config(
177
+ mock_config,
178
+ output_format="interactive",
179
+ verbose=False,
180
+ )
181
+
182
+ # Check client was created with correct config
183
+ MockClient.assert_called_once_with(mcp_config=mock_config, verbose=False)
184
+ mock_interactive.assert_called_once_with(mock_analysis)
185
+
186
+ @pytest.mark.asyncio
187
+ async def test_analyze_with_config_exception(self) -> None:
188
+ """Test config analysis handles exceptions gracefully."""
189
+ mock_config = {"server": {"command": "test"}}
190
+
191
+ with (
192
+ patch("hud.cli.analyze.MCPClient") as MockClient,
193
+ patch("hud.cli.analyze.console"),
194
+ ):
195
+ # Setup mock client that fails
196
+ mock_client = MagicMock()
197
+ mock_client.initialize = AsyncMock(side_effect=Exception("Test error"))
198
+ mock_client.shutdown = AsyncMock()
199
+ MockClient.return_value = mock_client
200
+
201
+ # Should not raise
202
+ await _analyze_with_config(
203
+ mock_config,
204
+ output_format="json",
205
+ verbose=False,
206
+ )
207
+
208
+ mock_client.shutdown.assert_called_once()
209
+
210
+ @pytest.mark.asyncio
211
+ async def test_analyze_environment_from_config(self) -> None:
212
+ """Test analyze_environment_from_config."""
213
+ config_data = {"server": {"command": "test"}}
214
+ mock_path = Path("test.json")
215
+
216
+ with (
217
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
218
+ patch("hud.cli.analyze._analyze_with_config") as mock_analyze,
219
+ ):
220
+ await analyze_environment_from_config(mock_path, "json", False)
221
+
222
+ mock_analyze.assert_called_once_with(config_data, "json", False)
223
+
224
+ @pytest.mark.asyncio
225
+ async def test_analyze_environment_from_mcp_config(self) -> None:
226
+ """Test analyze_environment_from_mcp_config."""
227
+ config_data = {"server": {"command": "test"}}
228
+
229
+ with patch("hud.cli.analyze._analyze_with_config") as mock_analyze:
230
+ await analyze_environment_from_mcp_config(config_data, "markdown", True)
231
+
232
+ mock_analyze.assert_called_once_with(config_data, "markdown", True)
233
+
234
+
235
+ class TestDisplayFunctions:
236
+ """Test display formatting functions."""
237
+
238
+ def test_display_interactive_basic(self) -> None:
239
+ """Test basic interactive display."""
240
+ analysis = {
241
+ "metadata": {"servers": ["test"], "initialized": True},
242
+ "tools": [{"name": "tool1", "description": "Test tool"}],
243
+ "hub_tools": {"hub1": ["func1", "func2"]},
244
+ "resources": [{"uri": "file:///test", "name": "Test", "description": "Resource"}],
245
+ "telemetry": {"status": "running", "live_url": "http://test"},
246
+ }
247
+
248
+ with patch("hud.cli.analyze.console") as mock_console:
249
+ display_interactive(analysis)
250
+
251
+ # Check console was called multiple times
252
+ assert mock_console.print.call_count > 0
253
+ # The design.section_title uses its own console, not the patched one
254
+ # Just verify the function ran without errors
255
+
256
+ def test_display_markdown_basic(self) -> None:
257
+ """Test basic markdown display."""
258
+ analysis = {
259
+ "metadata": {"servers": ["test1", "test2"], "initialized": True},
260
+ "tools": [
261
+ {"name": "tool1", "description": "Tool 1"},
262
+ {"name": "setup", "description": "Hub tool"},
263
+ ],
264
+ "hub_tools": {"setup": ["init", "config"]},
265
+ "resources": [{"uri": "telemetry://live", "name": "Telemetry"}],
266
+ "telemetry": {"status": "active"},
267
+ }
268
+
269
+ with patch("hud.cli.analyze.console") as mock_console:
270
+ display_markdown(analysis)
271
+
272
+ # Get the markdown output
273
+ mock_console.print.assert_called_once()
274
+ markdown = mock_console.print.call_args[0][0]
275
+
276
+ # Check markdown structure
277
+ assert "# MCP Environment Analysis" in markdown
278
+ assert "## Environment Overview" in markdown
279
+ assert "## Available Tools" in markdown
280
+ assert "### Regular Tools" in markdown
281
+ assert "### Hub Tools" in markdown
282
+ assert "- **tool1**: Tool 1" in markdown
283
+ assert "- **setup**" in markdown
284
+ assert " - init" in markdown