hud-python 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +22 -89
- hud/agents/__init__.py +17 -0
- hud/agents/art.py +101 -0
- hud/agents/base.py +599 -0
- hud/{mcp → agents}/claude.py +373 -321
- hud/{mcp → agents}/langchain.py +250 -250
- hud/agents/misc/__init__.py +7 -0
- hud/{agent → agents}/misc/response_agent.py +80 -80
- hud/{mcp → agents}/openai.py +352 -334
- hud/agents/openai_chat_generic.py +154 -0
- hud/{mcp → agents}/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -0
- hud/agents/tests/test_claude.py +324 -0
- hud/{mcp → agents}/tests/test_client.py +363 -324
- hud/{mcp → agents}/tests/test_openai.py +237 -238
- hud/cli/__init__.py +617 -0
- hud/cli/__main__.py +8 -0
- hud/cli/analyze.py +371 -0
- hud/cli/analyze_metadata.py +230 -0
- hud/cli/build.py +427 -0
- hud/cli/clone.py +185 -0
- hud/cli/cursor.py +92 -0
- hud/cli/debug.py +392 -0
- hud/cli/docker_utils.py +83 -0
- hud/cli/init.py +281 -0
- hud/cli/interactive.py +353 -0
- hud/cli/mcp_server.py +756 -0
- hud/cli/pull.py +336 -0
- hud/cli/push.py +379 -0
- hud/cli/remote_runner.py +311 -0
- hud/cli/runner.py +160 -0
- hud/cli/tests/__init__.py +3 -0
- hud/cli/tests/test_analyze.py +284 -0
- hud/cli/tests/test_cli_init.py +265 -0
- hud/cli/tests/test_cli_main.py +27 -0
- hud/cli/tests/test_clone.py +142 -0
- hud/cli/tests/test_cursor.py +253 -0
- hud/cli/tests/test_debug.py +453 -0
- hud/cli/tests/test_mcp_server.py +139 -0
- hud/cli/tests/test_utils.py +388 -0
- hud/cli/utils.py +263 -0
- hud/clients/README.md +143 -0
- hud/clients/__init__.py +16 -0
- hud/clients/base.py +354 -0
- hud/clients/fastmcp.py +202 -0
- hud/clients/mcp_use.py +278 -0
- hud/clients/tests/__init__.py +1 -0
- hud/clients/tests/test_client_integration.py +111 -0
- hud/clients/tests/test_fastmcp.py +342 -0
- hud/clients/tests/test_protocol.py +188 -0
- hud/clients/utils/__init__.py +1 -0
- hud/clients/utils/retry_transport.py +160 -0
- hud/datasets.py +322 -192
- hud/misc/__init__.py +1 -0
- hud/{agent → misc}/claude_plays_pokemon.py +292 -283
- hud/otel/__init__.py +35 -0
- hud/otel/collector.py +142 -0
- hud/otel/config.py +164 -0
- hud/otel/context.py +536 -0
- hud/otel/exporters.py +366 -0
- hud/otel/instrumentation.py +97 -0
- hud/otel/processors.py +118 -0
- hud/otel/tests/__init__.py +1 -0
- hud/otel/tests/test_processors.py +197 -0
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -0
- hud/server/helper/__init__.py +5 -0
- hud/server/low_level.py +132 -0
- hud/server/server.py +166 -0
- hud/server/tests/__init__.py +3 -0
- hud/settings.py +73 -79
- hud/shared/__init__.py +5 -0
- hud/{exceptions.py → shared/exceptions.py} +180 -180
- hud/{server → shared}/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -0
- hud/{server → shared}/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -30
- hud/telemetry/instrument.py +379 -0
- hud/telemetry/job.py +309 -141
- hud/telemetry/replay.py +74 -0
- hud/telemetry/trace.py +83 -0
- hud/tools/__init__.py +33 -34
- hud/tools/base.py +365 -65
- hud/tools/bash.py +161 -137
- hud/tools/computer/__init__.py +15 -13
- hud/tools/computer/anthropic.py +437 -420
- hud/tools/computer/hud.py +376 -334
- hud/tools/computer/openai.py +295 -292
- hud/tools/computer/settings.py +82 -0
- hud/tools/edit.py +314 -290
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -532
- hud/tools/executors/pyautogui.py +621 -619
- hud/tools/executors/tests/__init__.py +1 -1
- hud/tools/executors/tests/test_base_executor.py +338 -338
- hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
- hud/tools/executors/xdo.py +511 -503
- hud/tools/{playwright_tool.py → playwright.py} +412 -379
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -0
- hud/tools/tests/test_bash.py +158 -152
- hud/tools/tests/test_bash_extended.py +197 -0
- hud/tools/tests/test_computer.py +425 -52
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -240
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -157
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -0
- hud/tools/utils.py +50 -50
- hud/types.py +136 -89
- hud/utils/__init__.py +10 -16
- hud/utils/async_utils.py +65 -0
- hud/utils/design.py +168 -0
- hud/utils/mcp.py +55 -0
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -0
- hud/utils/tests/test_init.py +17 -21
- hud/utils/tests/test_progress.py +261 -225
- hud/utils/tests/test_telemetry.py +82 -37
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- hud_python-0.4.0.dist-info/METADATA +474 -0
- hud_python-0.4.0.dist-info/RECORD +132 -0
- hud_python-0.4.0.dist-info/entry_points.txt +3 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
- hud/adapters/__init__.py +0 -8
- hud/adapters/claude/__init__.py +0 -5
- hud/adapters/claude/adapter.py +0 -180
- hud/adapters/claude/tests/__init__.py +0 -1
- hud/adapters/claude/tests/test_adapter.py +0 -519
- hud/adapters/common/__init__.py +0 -6
- hud/adapters/common/adapter.py +0 -178
- hud/adapters/common/tests/test_adapter.py +0 -289
- hud/adapters/common/types.py +0 -446
- hud/adapters/operator/__init__.py +0 -5
- hud/adapters/operator/adapter.py +0 -108
- hud/adapters/operator/tests/__init__.py +0 -1
- hud/adapters/operator/tests/test_adapter.py +0 -370
- hud/agent/__init__.py +0 -19
- hud/agent/base.py +0 -126
- hud/agent/claude.py +0 -271
- hud/agent/langchain.py +0 -215
- hud/agent/misc/__init__.py +0 -3
- hud/agent/operator.py +0 -268
- hud/agent/tests/__init__.py +0 -1
- hud/agent/tests/test_base.py +0 -202
- hud/env/__init__.py +0 -11
- hud/env/client.py +0 -35
- hud/env/docker_client.py +0 -349
- hud/env/environment.py +0 -446
- hud/env/local_docker_client.py +0 -358
- hud/env/remote_client.py +0 -212
- hud/env/remote_docker_client.py +0 -292
- hud/gym.py +0 -130
- hud/job.py +0 -773
- hud/mcp/__init__.py +0 -17
- hud/mcp/base.py +0 -631
- hud/mcp/client.py +0 -312
- hud/mcp/tests/test_base.py +0 -512
- hud/mcp/tests/test_claude.py +0 -294
- hud/task.py +0 -149
- hud/taskset.py +0 -237
- hud/telemetry/_trace.py +0 -347
- hud/telemetry/context.py +0 -230
- hud/telemetry/exporter.py +0 -575
- hud/telemetry/instrumentation/__init__.py +0 -3
- hud/telemetry/instrumentation/mcp.py +0 -259
- hud/telemetry/instrumentation/registry.py +0 -59
- hud/telemetry/mcp_models.py +0 -270
- hud/telemetry/tests/__init__.py +0 -1
- hud/telemetry/tests/test_context.py +0 -210
- hud/telemetry/tests/test_trace.py +0 -312
- hud/tools/helper/README.md +0 -56
- hud/tools/helper/__init__.py +0 -9
- hud/tools/helper/mcp_server.py +0 -78
- hud/tools/helper/server_initialization.py +0 -115
- hud/tools/helper/utils.py +0 -58
- hud/trajectory.py +0 -94
- hud/utils/agent.py +0 -37
- hud/utils/common.py +0 -256
- hud/utils/config.py +0 -120
- hud/utils/deprecation.py +0 -115
- hud/utils/misc.py +0 -53
- hud/utils/tests/test_common.py +0 -277
- hud/utils/tests/test_config.py +0 -129
- hud_python-0.3.5.dist-info/METADATA +0 -284
- hud_python-0.3.5.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.5.dist-info → hud_python-0.4.0.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,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
|