hud-python 0.3.4__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 -414
- hud/tools/computer/hud.py +376 -328
- hud/tools/computer/openai.py +295 -286
- 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.4.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.4.dist-info/METADATA +0 -284
- hud_python-0.3.4.dist-info/RECORD +0 -120
- /hud/{adapters/common → shared}/tests/__init__.py +0 -0
- {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""Tests for hud.cli.utils module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from hud.cli.utils import HINT_REGISTRY, CaptureLogger, Colors, analyze_error_for_hints
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestColors:
|
|
14
|
+
"""Test ANSI color codes."""
|
|
15
|
+
|
|
16
|
+
def test_color_constants(self) -> None:
|
|
17
|
+
"""Test that color constants are defined."""
|
|
18
|
+
assert Colors.HEADER == "\033[95m"
|
|
19
|
+
assert Colors.BLUE == "\033[94m"
|
|
20
|
+
assert Colors.CYAN == "\033[96m"
|
|
21
|
+
assert Colors.GREEN == "\033[92m"
|
|
22
|
+
assert Colors.YELLOW == "\033[93m"
|
|
23
|
+
assert Colors.GOLD == "\033[33m"
|
|
24
|
+
assert Colors.RED == "\033[91m"
|
|
25
|
+
assert Colors.GRAY == "\033[90m"
|
|
26
|
+
assert Colors.ENDC == "\033[0m"
|
|
27
|
+
assert Colors.BOLD == "\033[1m"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestCaptureLogger:
|
|
31
|
+
"""Test CaptureLogger functionality."""
|
|
32
|
+
|
|
33
|
+
def test_logger_print_mode(self) -> None:
|
|
34
|
+
"""Test logger in print mode."""
|
|
35
|
+
logger = CaptureLogger(print_output=True)
|
|
36
|
+
|
|
37
|
+
with patch("builtins.print") as mock_print:
|
|
38
|
+
logger._log("Test message", Colors.GREEN)
|
|
39
|
+
mock_print.assert_called_once_with(f"{Colors.GREEN}Test message{Colors.ENDC}")
|
|
40
|
+
|
|
41
|
+
def test_logger_capture_mode(self) -> None:
|
|
42
|
+
"""Test logger in capture-only mode."""
|
|
43
|
+
logger = CaptureLogger(print_output=False)
|
|
44
|
+
|
|
45
|
+
with patch("builtins.print") as mock_print:
|
|
46
|
+
logger._log("Test message", Colors.GREEN)
|
|
47
|
+
mock_print.assert_not_called()
|
|
48
|
+
|
|
49
|
+
output = logger.get_output()
|
|
50
|
+
assert "Test message" in output
|
|
51
|
+
|
|
52
|
+
def test_strip_ansi(self) -> None:
|
|
53
|
+
"""Test ANSI code stripping."""
|
|
54
|
+
logger = CaptureLogger(print_output=False)
|
|
55
|
+
|
|
56
|
+
# Test various ANSI sequences
|
|
57
|
+
text_with_ansi = (
|
|
58
|
+
f"{Colors.GREEN}Green text{Colors.ENDC} normal {Colors.BOLD}bold{Colors.ENDC}"
|
|
59
|
+
)
|
|
60
|
+
clean_text = logger._strip_ansi(text_with_ansi)
|
|
61
|
+
assert clean_text == "Green text normal bold"
|
|
62
|
+
|
|
63
|
+
def test_timestamp(self) -> None:
|
|
64
|
+
"""Test timestamp generation."""
|
|
65
|
+
logger = CaptureLogger(print_output=False)
|
|
66
|
+
|
|
67
|
+
timestamp = logger.timestamp()
|
|
68
|
+
# Should be in HH:MM:SS format
|
|
69
|
+
assert len(timestamp) == 8
|
|
70
|
+
assert timestamp[2] == ":"
|
|
71
|
+
assert timestamp[5] == ":"
|
|
72
|
+
|
|
73
|
+
def test_phase_logging(self) -> None:
|
|
74
|
+
"""Test phase header logging."""
|
|
75
|
+
logger = CaptureLogger(print_output=False)
|
|
76
|
+
|
|
77
|
+
logger.phase(1, "Test Phase")
|
|
78
|
+
output = logger.get_output()
|
|
79
|
+
|
|
80
|
+
assert "=" * 80 in output
|
|
81
|
+
assert "PHASE 1: Test Phase" in output
|
|
82
|
+
|
|
83
|
+
def test_command_logging(self) -> None:
|
|
84
|
+
"""Test command logging."""
|
|
85
|
+
logger = CaptureLogger(print_output=False)
|
|
86
|
+
|
|
87
|
+
logger.command(["python", "script.py", "--arg", "value"])
|
|
88
|
+
output = logger.get_output()
|
|
89
|
+
|
|
90
|
+
assert "$ python script.py --arg value" in output
|
|
91
|
+
|
|
92
|
+
def test_success_logging(self) -> None:
|
|
93
|
+
"""Test success message logging."""
|
|
94
|
+
logger = CaptureLogger(print_output=False)
|
|
95
|
+
|
|
96
|
+
logger.success("Operation completed")
|
|
97
|
+
output = logger.get_output()
|
|
98
|
+
|
|
99
|
+
assert "✅ Operation completed" in output
|
|
100
|
+
|
|
101
|
+
def test_error_logging(self) -> None:
|
|
102
|
+
"""Test error message logging."""
|
|
103
|
+
logger = CaptureLogger(print_output=False)
|
|
104
|
+
|
|
105
|
+
logger.error("Operation failed")
|
|
106
|
+
output = logger.get_output()
|
|
107
|
+
|
|
108
|
+
assert "❌ Operation failed" in output
|
|
109
|
+
|
|
110
|
+
def test_info_logging(self) -> None:
|
|
111
|
+
"""Test info message logging with timestamp."""
|
|
112
|
+
logger = CaptureLogger(print_output=False)
|
|
113
|
+
|
|
114
|
+
with patch.object(logger, "timestamp", return_value="12:34:56"):
|
|
115
|
+
logger.info("Information message")
|
|
116
|
+
output = logger.get_output()
|
|
117
|
+
|
|
118
|
+
assert "[12:34:56] Information message" in output
|
|
119
|
+
|
|
120
|
+
def test_stdio_logging(self) -> None:
|
|
121
|
+
"""Test STDIO communication logging."""
|
|
122
|
+
logger = CaptureLogger(print_output=False)
|
|
123
|
+
|
|
124
|
+
logger.stdio("JSON-RPC message")
|
|
125
|
+
output = logger.get_output()
|
|
126
|
+
|
|
127
|
+
assert "[STDIO] JSON-RPC message" in output
|
|
128
|
+
|
|
129
|
+
def test_stderr_logging(self) -> None:
|
|
130
|
+
"""Test STDERR output logging."""
|
|
131
|
+
logger = CaptureLogger(print_output=False)
|
|
132
|
+
|
|
133
|
+
logger.stderr("Error output from server")
|
|
134
|
+
output = logger.get_output()
|
|
135
|
+
|
|
136
|
+
assert "[STDERR] Error output from server" in output
|
|
137
|
+
|
|
138
|
+
def test_hint_logging(self) -> None:
|
|
139
|
+
"""Test hint message logging."""
|
|
140
|
+
logger = CaptureLogger(print_output=False)
|
|
141
|
+
|
|
142
|
+
logger.hint("Try checking the configuration")
|
|
143
|
+
output = logger.get_output()
|
|
144
|
+
|
|
145
|
+
assert "💡 Hint: Try checking the configuration" in output
|
|
146
|
+
|
|
147
|
+
def test_progress_bar(self) -> None:
|
|
148
|
+
"""Test progress bar visualization."""
|
|
149
|
+
logger = CaptureLogger(print_output=False)
|
|
150
|
+
|
|
151
|
+
# Test partial progress
|
|
152
|
+
logger.progress_bar(3, 5)
|
|
153
|
+
output = logger.get_output()
|
|
154
|
+
|
|
155
|
+
assert "Progress: [███░░] 3/5 phases (60%)" in output
|
|
156
|
+
assert "Failed at Phase 4" in output
|
|
157
|
+
|
|
158
|
+
# Test complete progress
|
|
159
|
+
logger = CaptureLogger(print_output=False)
|
|
160
|
+
logger.progress_bar(5, 5)
|
|
161
|
+
output = logger.get_output()
|
|
162
|
+
|
|
163
|
+
assert "Progress: [█████] 5/5 phases (100%)" in output
|
|
164
|
+
assert "All phases completed successfully!" in output
|
|
165
|
+
|
|
166
|
+
def test_progress_bar_failure_messages(self) -> None:
|
|
167
|
+
"""Test progress bar failure messages at different phases."""
|
|
168
|
+
test_cases = [
|
|
169
|
+
(0, "Failed at Phase 1 - Server startup"),
|
|
170
|
+
(1, "Failed at Phase 2 - MCP initialization"),
|
|
171
|
+
(2, "Failed at Phase 3 - Tool discovery"),
|
|
172
|
+
(3, "Failed at Phase 4 - Remote deployment readiness"),
|
|
173
|
+
(4, "Failed at Phase 5 - Concurrent clients & resources"),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
for completed, expected_msg in test_cases:
|
|
177
|
+
logger = CaptureLogger(print_output=False)
|
|
178
|
+
logger.progress_bar(completed, 5)
|
|
179
|
+
output = logger.get_output()
|
|
180
|
+
assert expected_msg in output
|
|
181
|
+
|
|
182
|
+
def test_get_output(self) -> None:
|
|
183
|
+
"""Test getting accumulated output."""
|
|
184
|
+
logger = CaptureLogger(print_output=False)
|
|
185
|
+
|
|
186
|
+
logger.info("First message")
|
|
187
|
+
logger.error("Second message")
|
|
188
|
+
logger.success("Third message")
|
|
189
|
+
|
|
190
|
+
output = logger.get_output()
|
|
191
|
+
assert "First message" in output
|
|
192
|
+
assert "Second message" in output
|
|
193
|
+
assert "Third message" in output
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class TestAnalyzeErrorForHints:
|
|
197
|
+
"""Test error analysis and hint generation."""
|
|
198
|
+
|
|
199
|
+
def test_x11_display_errors(self) -> None:
|
|
200
|
+
"""Test X11/display related error hints."""
|
|
201
|
+
errors = [
|
|
202
|
+
"Can't connect to display :0",
|
|
203
|
+
"X11 connection rejected",
|
|
204
|
+
"DISPLAY environment variable not set",
|
|
205
|
+
"Xlib.error.DisplayConnectionError",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
for error in errors:
|
|
209
|
+
hint = analyze_error_for_hints(error)
|
|
210
|
+
assert hint is not None
|
|
211
|
+
assert "GUI environment needs X11" in hint
|
|
212
|
+
assert "Xvfb" in hint
|
|
213
|
+
|
|
214
|
+
def test_import_errors(self) -> None:
|
|
215
|
+
"""Test import/module error hints."""
|
|
216
|
+
errors = [
|
|
217
|
+
"ModuleNotFoundError: No module named 'requests'",
|
|
218
|
+
"ImportError: cannot import name 'api'",
|
|
219
|
+
"No module named numpy",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for error in errors:
|
|
223
|
+
hint = analyze_error_for_hints(error)
|
|
224
|
+
assert hint is not None
|
|
225
|
+
assert "Missing Python dependencies" in hint
|
|
226
|
+
assert "pyproject.toml" in hint
|
|
227
|
+
|
|
228
|
+
def test_json_errors(self) -> None:
|
|
229
|
+
"""Test JSON parsing error hints."""
|
|
230
|
+
errors = [
|
|
231
|
+
"json.decoder.JSONDecodeError: Expecting value",
|
|
232
|
+
"JSONDecodeError: Expecting value: line 1 column 1",
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
for error in errors:
|
|
236
|
+
hint = analyze_error_for_hints(error)
|
|
237
|
+
assert hint is not None
|
|
238
|
+
assert "Invalid JSON-RPC communication" in hint
|
|
239
|
+
assert "proper JSON-RPC format" in hint
|
|
240
|
+
|
|
241
|
+
def test_permission_errors(self) -> None:
|
|
242
|
+
"""Test permission error hints."""
|
|
243
|
+
errors = [
|
|
244
|
+
"Permission denied: /var/log/app.log",
|
|
245
|
+
"EACCES: permission denied",
|
|
246
|
+
"Operation not permitted",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for error in errors:
|
|
250
|
+
hint = analyze_error_for_hints(error)
|
|
251
|
+
assert hint is not None
|
|
252
|
+
assert "Permission issues" in hint
|
|
253
|
+
assert "Check file permissions" in hint
|
|
254
|
+
|
|
255
|
+
def test_memory_errors(self) -> None:
|
|
256
|
+
"""Test memory/resource error hints."""
|
|
257
|
+
errors = ["Cannot allocate memory", "Process killed", "Container OOMKilled"]
|
|
258
|
+
|
|
259
|
+
for error in errors:
|
|
260
|
+
hint = analyze_error_for_hints(error)
|
|
261
|
+
assert hint is not None
|
|
262
|
+
assert "Resource limits exceeded" in hint
|
|
263
|
+
assert "memory limits" in hint
|
|
264
|
+
|
|
265
|
+
def test_port_errors(self) -> None:
|
|
266
|
+
"""Test port binding error hints."""
|
|
267
|
+
errors = [
|
|
268
|
+
"bind: address already in use",
|
|
269
|
+
"EADDRINUSE: address already in use",
|
|
270
|
+
"port 8080 already allocated",
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
for error in errors:
|
|
274
|
+
hint = analyze_error_for_hints(error)
|
|
275
|
+
assert hint is not None
|
|
276
|
+
assert "Port conflict detected" in hint
|
|
277
|
+
assert "different port" in hint
|
|
278
|
+
|
|
279
|
+
def test_file_not_found_errors(self) -> None:
|
|
280
|
+
"""Test file not found error hints."""
|
|
281
|
+
errors = [
|
|
282
|
+
"FileNotFoundError: [Errno 2] No such file or directory",
|
|
283
|
+
"No such file or directory: config.json",
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
for error in errors:
|
|
287
|
+
hint = analyze_error_for_hints(error)
|
|
288
|
+
assert hint is not None
|
|
289
|
+
assert "File or directory missing" in hint
|
|
290
|
+
assert "required files exist" in hint
|
|
291
|
+
|
|
292
|
+
def test_traceback_errors(self) -> None:
|
|
293
|
+
"""Test general traceback error hints."""
|
|
294
|
+
error = """Traceback (most recent call last):
|
|
295
|
+
File "app.py", line 10, in <module>
|
|
296
|
+
import missing_module
|
|
297
|
+
ImportError: No module named missing_module"""
|
|
298
|
+
|
|
299
|
+
hint = analyze_error_for_hints(error)
|
|
300
|
+
assert hint is not None
|
|
301
|
+
# Should match both traceback and import patterns
|
|
302
|
+
# Import has higher priority
|
|
303
|
+
assert "Missing Python dependencies" in hint
|
|
304
|
+
|
|
305
|
+
def test_timeout_errors(self) -> None:
|
|
306
|
+
"""Test timeout error hints."""
|
|
307
|
+
errors = ["Operation timed out after 30 seconds", "Connection timeout", "Request timed out"]
|
|
308
|
+
|
|
309
|
+
for error in errors:
|
|
310
|
+
hint = analyze_error_for_hints(error)
|
|
311
|
+
assert hint is not None
|
|
312
|
+
assert "Server taking too long to start" in hint
|
|
313
|
+
assert "slow operations" in hint
|
|
314
|
+
|
|
315
|
+
def test_no_error_text(self) -> None:
|
|
316
|
+
"""Test with empty or None error text."""
|
|
317
|
+
assert analyze_error_for_hints("") is None
|
|
318
|
+
assert analyze_error_for_hints(None) is None
|
|
319
|
+
|
|
320
|
+
def test_no_matching_pattern(self) -> None:
|
|
321
|
+
"""Test with error that doesn't match any pattern."""
|
|
322
|
+
hint = analyze_error_for_hints("Some random error message")
|
|
323
|
+
assert hint is None
|
|
324
|
+
|
|
325
|
+
def test_priority_ordering(self) -> None:
|
|
326
|
+
"""Test that higher priority hints are returned."""
|
|
327
|
+
# This error matches both "No module" (priority 9) and "Exception" (priority 2)
|
|
328
|
+
error = "Exception: No module named requests"
|
|
329
|
+
hint = analyze_error_for_hints(error)
|
|
330
|
+
assert hint is not None
|
|
331
|
+
# Should get the higher priority hint (import error)
|
|
332
|
+
assert "Missing Python dependencies" in hint
|
|
333
|
+
|
|
334
|
+
def test_case_insensitive_matching(self) -> None:
|
|
335
|
+
"""Test that pattern matching is case insensitive."""
|
|
336
|
+
errors = ["PERMISSION DENIED", "permission denied", "Permission Denied"]
|
|
337
|
+
|
|
338
|
+
for error in errors:
|
|
339
|
+
hint = analyze_error_for_hints(error)
|
|
340
|
+
assert hint is not None
|
|
341
|
+
assert "Permission issues" in hint
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class TestHintRegistry:
|
|
345
|
+
"""Test the hint registry structure."""
|
|
346
|
+
|
|
347
|
+
def test_hint_registry_structure(self) -> None:
|
|
348
|
+
"""Test that HINT_REGISTRY has correct structure."""
|
|
349
|
+
assert isinstance(HINT_REGISTRY, list)
|
|
350
|
+
assert len(HINT_REGISTRY) > 0
|
|
351
|
+
|
|
352
|
+
for hint_data in HINT_REGISTRY:
|
|
353
|
+
assert "patterns" in hint_data
|
|
354
|
+
assert "priority" in hint_data
|
|
355
|
+
assert "hint" in hint_data
|
|
356
|
+
|
|
357
|
+
assert isinstance(hint_data["patterns"], list)
|
|
358
|
+
assert isinstance(hint_data["priority"], int)
|
|
359
|
+
assert isinstance(hint_data["hint"], str)
|
|
360
|
+
|
|
361
|
+
# All patterns should be strings
|
|
362
|
+
for pattern in hint_data["patterns"]:
|
|
363
|
+
assert isinstance(pattern, str)
|
|
364
|
+
|
|
365
|
+
def test_hint_priorities_unique(self) -> None:
|
|
366
|
+
"""Test that hint priorities are reasonable."""
|
|
367
|
+
priorities = [hint["priority"] for hint in HINT_REGISTRY]
|
|
368
|
+
|
|
369
|
+
# Priorities should be positive
|
|
370
|
+
assert all(p > 0 for p in priorities)
|
|
371
|
+
|
|
372
|
+
# Should have a range of priorities
|
|
373
|
+
assert max(priorities) > min(priorities)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestWindowsSupport:
|
|
377
|
+
"""Test Windows-specific functionality."""
|
|
378
|
+
|
|
379
|
+
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only test")
|
|
380
|
+
def test_windows_ansi_enable(self) -> None:
|
|
381
|
+
"""Test that ANSI is enabled on Windows."""
|
|
382
|
+
# The module should call os.system("") on import
|
|
383
|
+
# This is hard to test directly, but we can check platform detection
|
|
384
|
+
assert sys.platform == "win32"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
pytest.main([__file__])
|
hud/cli/utils.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""CLI utilities - logging, colors, and error analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from io import StringIO
|
|
9
|
+
|
|
10
|
+
# Enable ANSI colors on Windows
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
os.system("") # Enable ANSI escape sequences on Windows # noqa: S607 S605
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Colors:
|
|
18
|
+
"""ANSI color codes for terminal output."""
|
|
19
|
+
|
|
20
|
+
HEADER = "\033[95m"
|
|
21
|
+
BLUE = "\033[94m"
|
|
22
|
+
CYAN = "\033[96m"
|
|
23
|
+
GREEN = "\033[92m"
|
|
24
|
+
YELLOW = "\033[93m"
|
|
25
|
+
GOLD = "\033[33m"
|
|
26
|
+
RED = "\033[91m"
|
|
27
|
+
GRAY = "\033[90m"
|
|
28
|
+
ENDC = "\033[0m"
|
|
29
|
+
BOLD = "\033[1m"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CaptureLogger:
|
|
33
|
+
"""Logger that can both print and capture output."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, print_output: bool = True) -> None:
|
|
36
|
+
self.print_output = print_output
|
|
37
|
+
self.buffer = StringIO()
|
|
38
|
+
|
|
39
|
+
def _log(self, message: str, color: str = "") -> None:
|
|
40
|
+
"""Internal log method that handles both printing and capturing."""
|
|
41
|
+
if self.print_output:
|
|
42
|
+
if color:
|
|
43
|
+
print(f"{color}{message}{Colors.ENDC}") # noqa: T201
|
|
44
|
+
else:
|
|
45
|
+
print(message) # noqa: T201
|
|
46
|
+
|
|
47
|
+
# Always capture (without ANSI codes)
|
|
48
|
+
clean_msg = self._strip_ansi(message)
|
|
49
|
+
self.buffer.write(clean_msg + "\n")
|
|
50
|
+
|
|
51
|
+
def _strip_ansi(self, text: str) -> str:
|
|
52
|
+
"""Remove ANSI escape codes from text."""
|
|
53
|
+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
54
|
+
return ansi_escape.sub("", text)
|
|
55
|
+
|
|
56
|
+
def timestamp(self) -> str:
|
|
57
|
+
"""Get minimal timestamp HH:MM:SS."""
|
|
58
|
+
return datetime.now().strftime("%H:%M:%S")
|
|
59
|
+
|
|
60
|
+
def phase(self, phase_num: int, title: str) -> None:
|
|
61
|
+
"""Log a phase header."""
|
|
62
|
+
self._log(f"\n{'=' * 80}", Colors.GOLD if self.print_output else "")
|
|
63
|
+
self._log(
|
|
64
|
+
f"PHASE {phase_num}: {title}", Colors.BOLD + Colors.GOLD if self.print_output else ""
|
|
65
|
+
)
|
|
66
|
+
self._log(f"{'=' * 80}\n", Colors.GOLD if self.print_output else "")
|
|
67
|
+
|
|
68
|
+
def command(self, cmd: list) -> None:
|
|
69
|
+
"""Log the command being executed."""
|
|
70
|
+
self._log(f"$ {' '.join(cmd)}", Colors.BOLD if self.print_output else "")
|
|
71
|
+
|
|
72
|
+
def success(self, message: str) -> None:
|
|
73
|
+
"""Log a success message."""
|
|
74
|
+
self._log(f"✅ {message}", Colors.GREEN if self.print_output else "")
|
|
75
|
+
|
|
76
|
+
def error(self, message: str) -> None:
|
|
77
|
+
"""Log an error message."""
|
|
78
|
+
self._log(f"❌ {message}", Colors.RED if self.print_output else "")
|
|
79
|
+
|
|
80
|
+
def info(self, message: str) -> None:
|
|
81
|
+
"""Log an info message."""
|
|
82
|
+
self._log(f"[{self.timestamp()}] {message}")
|
|
83
|
+
|
|
84
|
+
def stdio(self, message: str) -> None:
|
|
85
|
+
"""Log STDIO communication."""
|
|
86
|
+
self._log(f"[STDIO] {message}", Colors.GOLD if self.print_output else "")
|
|
87
|
+
|
|
88
|
+
def stderr(self, message: str) -> None:
|
|
89
|
+
"""Log STDERR output."""
|
|
90
|
+
self._log(f"[STDERR] {message}", Colors.GRAY if self.print_output else "")
|
|
91
|
+
|
|
92
|
+
def hint(self, hint: str) -> None:
|
|
93
|
+
"""Log a hint message."""
|
|
94
|
+
self._log(f"\n💡 Hint: {hint}", Colors.YELLOW if self.print_output else "")
|
|
95
|
+
|
|
96
|
+
def progress_bar(self, completed: int, total: int) -> None:
|
|
97
|
+
"""Show a visual progress bar."""
|
|
98
|
+
filled = "█" * completed
|
|
99
|
+
empty = "░" * (total - completed)
|
|
100
|
+
percentage = (completed / total) * 100
|
|
101
|
+
|
|
102
|
+
self._log(
|
|
103
|
+
f"\nProgress: [{filled}{empty}] {completed}/{total} phases ({percentage:.0f}%)",
|
|
104
|
+
Colors.BOLD if self.print_output else "",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
phase_messages = {
|
|
108
|
+
0: ("Failed at Phase 1 - Server startup", Colors.RED),
|
|
109
|
+
1: ("Failed at Phase 2 - MCP initialization", Colors.YELLOW),
|
|
110
|
+
2: ("Failed at Phase 3 - Tool discovery", Colors.YELLOW),
|
|
111
|
+
3: ("Failed at Phase 4 - Remote deployment readiness", Colors.YELLOW),
|
|
112
|
+
4: ("Failed at Phase 5 - Concurrent clients & resources", Colors.YELLOW),
|
|
113
|
+
5: ("All phases completed successfully!", Colors.GREEN),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if completed in phase_messages:
|
|
117
|
+
msg, color = phase_messages[completed]
|
|
118
|
+
self._log(msg, color if self.print_output else "")
|
|
119
|
+
|
|
120
|
+
def get_output(self) -> str:
|
|
121
|
+
"""Get the captured output."""
|
|
122
|
+
return self.buffer.getvalue()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Hint registry with patterns and priorities
|
|
126
|
+
HINT_REGISTRY = [
|
|
127
|
+
{
|
|
128
|
+
"patterns": [r"Can't connect to display", r"X11", r"DISPLAY.*not set", r"Xlib.*error"],
|
|
129
|
+
"priority": 10,
|
|
130
|
+
"hint": """GUI environment needs X11. Common fixes:
|
|
131
|
+
- Start Xvfb before importing GUI libraries in your entrypoint
|
|
132
|
+
- Use a base image with X11 pre-configured (e.g., hudpython/novnc-base)
|
|
133
|
+
- Delay GUI imports until after X11 is running""",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"patterns": [r"ModuleNotFoundError", r"ImportError", r"No module named"],
|
|
137
|
+
"priority": 9,
|
|
138
|
+
"hint": """Missing Python dependencies. Check:
|
|
139
|
+
- Is pyproject.toml complete with all dependencies?
|
|
140
|
+
- Did 'pip install' run successfully?
|
|
141
|
+
- For editable installs, is the package structure correct?""",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"patterns": [r"json\.decoder\.JSONDecodeError", r"Expecting value.*line.*column"],
|
|
145
|
+
"priority": 8,
|
|
146
|
+
"hint": """Invalid JSON-RPC communication. Check:
|
|
147
|
+
- MCP server is using proper JSON-RPC format
|
|
148
|
+
- No debug prints are corrupting stdout
|
|
149
|
+
- Character encoding is UTF-8""",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"patterns": [r"Permission denied", r"EACCES", r"Operation not permitted"],
|
|
153
|
+
"priority": 7,
|
|
154
|
+
"hint": """Permission issues. Try:
|
|
155
|
+
- Check file permissions in container/environment
|
|
156
|
+
- Running with appropriate user
|
|
157
|
+
- Using --privileged flag if absolutely needed (Docker)""",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"patterns": [r"Cannot allocate memory", r"killed", r"OOMKilled"],
|
|
161
|
+
"priority": 6,
|
|
162
|
+
"hint": """Resource limits exceeded. Consider:
|
|
163
|
+
- Increasing memory limits
|
|
164
|
+
- Optimizing memory usage in your code
|
|
165
|
+
- Checking for memory leaks""",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"patterns": [r"bind.*address already in use", r"EADDRINUSE", r"port.*already allocated"],
|
|
169
|
+
"priority": 5,
|
|
170
|
+
"hint": """Port conflict detected. Options:
|
|
171
|
+
- Use a different port
|
|
172
|
+
- Check if another process is running
|
|
173
|
+
- Ensure proper cleanup in previous runs""",
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"patterns": [r"FileNotFoundError", r"No such file or directory"],
|
|
177
|
+
"priority": 4,
|
|
178
|
+
"hint": """File or directory missing. Check:
|
|
179
|
+
- All required files exist
|
|
180
|
+
- Working directory is set correctly
|
|
181
|
+
- File paths are correct for the environment""",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"patterns": [r"Traceback.*most recent call last", r"Exception"],
|
|
185
|
+
"priority": 2,
|
|
186
|
+
"hint": """Server crashed during startup. Common causes:
|
|
187
|
+
- Missing environment variables
|
|
188
|
+
- Import errors in your module
|
|
189
|
+
- Initialization code failing""",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"patterns": [r"timeout", r"timed out"],
|
|
193
|
+
"priority": 1,
|
|
194
|
+
"hint": """Server taking too long to start. Consider:
|
|
195
|
+
- Using initialization wrappers for heavy setup
|
|
196
|
+
- Moving slow operations to setup() tool
|
|
197
|
+
- Checking for deadlocks or infinite loops""",
|
|
198
|
+
},
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def analyze_error_for_hints(error_text: str | None) -> str | None:
|
|
203
|
+
"""Analyze error text and return the highest priority matching hint."""
|
|
204
|
+
if not error_text:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
matches = []
|
|
208
|
+
for hint_data in HINT_REGISTRY:
|
|
209
|
+
for pattern in hint_data["patterns"]:
|
|
210
|
+
if re.search(pattern, error_text, re.IGNORECASE):
|
|
211
|
+
matches.append((hint_data["priority"], hint_data["hint"]))
|
|
212
|
+
break
|
|
213
|
+
|
|
214
|
+
if matches:
|
|
215
|
+
matches.sort(key=lambda x: x[0], reverse=True)
|
|
216
|
+
return matches[0][1]
|
|
217
|
+
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def find_free_port(start_port: int = 8765, max_attempts: int = 100) -> int | None:
|
|
222
|
+
"""Find a free port starting from the given port.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
start_port: Port to start searching from
|
|
226
|
+
max_attempts: Maximum number of ports to try
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Available port number or None if no ports found
|
|
230
|
+
"""
|
|
231
|
+
import socket
|
|
232
|
+
|
|
233
|
+
for port in range(start_port, start_port + max_attempts):
|
|
234
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
235
|
+
try:
|
|
236
|
+
# Try to bind to the port
|
|
237
|
+
s.bind(("", port))
|
|
238
|
+
s.close()
|
|
239
|
+
return port
|
|
240
|
+
except OSError:
|
|
241
|
+
# Port is in use, try next one
|
|
242
|
+
continue
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def is_port_free(port: int) -> bool:
|
|
247
|
+
"""Check if a specific port is free.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
port: Port number to check
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
True if port is free, False otherwise
|
|
254
|
+
"""
|
|
255
|
+
import socket
|
|
256
|
+
|
|
257
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
258
|
+
try:
|
|
259
|
+
s.bind(("", port))
|
|
260
|
+
s.close()
|
|
261
|
+
return True
|
|
262
|
+
except OSError:
|
|
263
|
+
return False
|