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
hud/cli/runner.py ADDED
@@ -0,0 +1,160 @@
1
+ """Run Docker images as MCP servers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import subprocess
7
+ import sys
8
+
9
+ import click
10
+ from fastmcp import FastMCP
11
+
12
+ from hud.utils.design import HUDDesign
13
+
14
+
15
+ def run_stdio_server(image: str, docker_args: list[str], verbose: bool) -> None:
16
+ """Run Docker image as stdio MCP server (direct passthrough)."""
17
+ design = HUDDesign(stderr=True) # Use stderr for stdio mode
18
+
19
+ # Build docker command
20
+ docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
21
+
22
+ if verbose:
23
+ design.info(f"🐳 Running: {' '.join(docker_cmd)}")
24
+
25
+ # Run docker directly with stdio passthrough
26
+ try:
27
+ result = subprocess.run(docker_cmd, stdin=sys.stdin) # noqa: S603
28
+ sys.exit(result.returncode)
29
+ except KeyboardInterrupt:
30
+ design.info("\n👋 Shutting down...")
31
+ sys.exit(0)
32
+ except Exception as e:
33
+ design.error(f"Error: {e}")
34
+ sys.exit(1)
35
+
36
+
37
+ async def run_http_server(image: str, docker_args: list[str], port: int, verbose: bool) -> None:
38
+ """Run Docker image as HTTP MCP server (proxy mode)."""
39
+ from .utils import find_free_port
40
+
41
+ design = HUDDesign()
42
+
43
+ # Find available port
44
+ actual_port = find_free_port(port)
45
+ if actual_port is None:
46
+ design.error(f"No available ports found starting from {port}")
47
+ return
48
+
49
+ if actual_port != port:
50
+ design.warning(f"Port {port} in use, using port {actual_port} instead")
51
+
52
+ # Generate container name
53
+ container_name = f"run-{image.replace(':', '-').replace('/', '-')}"
54
+
55
+ # Remove any existing container with the same name
56
+ try:
57
+ subprocess.run( # noqa: ASYNC221, S603
58
+ ["docker", "rm", "-f", container_name], # noqa: S607
59
+ stdout=subprocess.DEVNULL,
60
+ stderr=subprocess.DEVNULL,
61
+ check=False, # Don't raise error if container doesn't exist
62
+ )
63
+ except Exception:
64
+ click.echo(f"Failed to remove existing container {container_name}", err=True)
65
+
66
+ # Build docker command for stdio container
67
+ docker_cmd = (
68
+ *[
69
+ "docker",
70
+ "run",
71
+ "--rm",
72
+ "-i",
73
+ "--name",
74
+ container_name,
75
+ ],
76
+ docker_args,
77
+ [image],
78
+ )
79
+
80
+ # Create MCP config for stdio transport
81
+ config = {
82
+ "mcpServers": {
83
+ "default": {
84
+ "command": docker_cmd[0],
85
+ "args": docker_cmd[1:] if len(docker_cmd) > 1 else [],
86
+ # transport defaults to stdio
87
+ }
88
+ }
89
+ }
90
+
91
+ # Set up logging suppression
92
+ import logging
93
+ import os
94
+
95
+ os.environ["FASTMCP_DISABLE_BANNER"] = "1"
96
+
97
+ if not verbose:
98
+ logging.getLogger("fastmcp").setLevel(logging.ERROR)
99
+ logging.getLogger("mcp").setLevel(logging.ERROR)
100
+ logging.getLogger("uvicorn").setLevel(logging.ERROR)
101
+ logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
102
+ logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
103
+
104
+ import warnings
105
+
106
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
107
+
108
+ # Create HTTP proxy
109
+ proxy = FastMCP.as_proxy(config, name=f"HUD Run - {image}")
110
+
111
+ # Show header
112
+ design.info("") # Empty line
113
+ design.header("HUD MCP Server", icon="🌐")
114
+
115
+ # Show configuration
116
+ design.section_title("Server Information")
117
+ design.info(f"Port: {actual_port}")
118
+ design.info(f"URL: http://localhost:{actual_port}/mcp")
119
+ design.info(f"Container: {container_name}")
120
+ design.info("")
121
+ design.progress_message("Press Ctrl+C to stop")
122
+
123
+ try:
124
+ await proxy.run_async(
125
+ transport="http",
126
+ host="0.0.0.0", # noqa: S104
127
+ port=actual_port,
128
+ path="/mcp",
129
+ log_level="error" if not verbose else "info",
130
+ show_banner=False,
131
+ )
132
+ except KeyboardInterrupt:
133
+ design.info("\n👋 Shutting down...")
134
+
135
+
136
+ def run_mcp_server(
137
+ image: str, docker_args: list[str], transport: str, port: int, verbose: bool
138
+ ) -> None:
139
+ """Run Docker image as MCP server with specified transport."""
140
+ if transport == "stdio":
141
+ run_stdio_server(image, docker_args, verbose)
142
+ elif transport == "http":
143
+ try:
144
+ asyncio.run(run_http_server(image, docker_args, port, verbose))
145
+ except Exception as e:
146
+ # Suppress the graceful shutdown errors
147
+ if not any(
148
+ x in str(e)
149
+ for x in [
150
+ "timeout graceful shutdown exceeded",
151
+ "Cancel 0 running task(s)",
152
+ "Application shutdown complete",
153
+ ]
154
+ ):
155
+ design = HUDDesign()
156
+ design.error(f"Unexpected error: {e}")
157
+ else:
158
+ design = HUDDesign()
159
+ design.error(f"Unknown transport: {transport}")
160
+ sys.exit(1)
@@ -0,0 +1,3 @@
1
+ """Tests for HUD CLI module."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +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