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.
- hud/__init__.py +22 -22
- hud/agents/__init__.py +13 -15
- hud/agents/base.py +599 -599
- hud/agents/claude.py +373 -373
- hud/agents/langchain.py +261 -250
- hud/agents/misc/__init__.py +7 -7
- hud/agents/misc/response_agent.py +82 -80
- hud/agents/openai.py +352 -352
- hud/agents/openai_chat_generic.py +154 -154
- hud/agents/tests/__init__.py +1 -1
- hud/agents/tests/test_base.py +742 -742
- hud/agents/tests/test_claude.py +324 -324
- hud/agents/tests/test_client.py +363 -363
- hud/agents/tests/test_openai.py +237 -237
- hud/cli/__init__.py +617 -617
- hud/cli/__main__.py +8 -8
- hud/cli/analyze.py +371 -371
- hud/cli/analyze_metadata.py +230 -230
- hud/cli/build.py +498 -427
- hud/cli/clone.py +185 -185
- hud/cli/cursor.py +92 -92
- hud/cli/debug.py +392 -392
- hud/cli/docker_utils.py +83 -83
- hud/cli/init.py +280 -281
- hud/cli/interactive.py +353 -353
- hud/cli/mcp_server.py +764 -756
- hud/cli/pull.py +330 -336
- hud/cli/push.py +404 -370
- hud/cli/remote_runner.py +311 -311
- hud/cli/runner.py +160 -160
- hud/cli/tests/__init__.py +3 -3
- hud/cli/tests/test_analyze.py +284 -284
- hud/cli/tests/test_cli_init.py +265 -265
- hud/cli/tests/test_cli_main.py +27 -27
- hud/cli/tests/test_clone.py +142 -142
- hud/cli/tests/test_cursor.py +253 -253
- hud/cli/tests/test_debug.py +453 -453
- hud/cli/tests/test_mcp_server.py +139 -139
- hud/cli/tests/test_utils.py +388 -388
- hud/cli/utils.py +263 -263
- hud/clients/README.md +143 -143
- hud/clients/__init__.py +16 -16
- hud/clients/base.py +378 -379
- hud/clients/fastmcp.py +222 -222
- hud/clients/mcp_use.py +298 -278
- hud/clients/tests/__init__.py +1 -1
- hud/clients/tests/test_client_integration.py +111 -111
- hud/clients/tests/test_fastmcp.py +342 -342
- hud/clients/tests/test_protocol.py +188 -188
- hud/clients/utils/__init__.py +1 -1
- hud/clients/utils/retry_transport.py +160 -160
- hud/datasets.py +327 -322
- hud/misc/__init__.py +1 -1
- hud/misc/claude_plays_pokemon.py +292 -292
- hud/otel/__init__.py +35 -35
- hud/otel/collector.py +142 -142
- hud/otel/config.py +164 -164
- hud/otel/context.py +536 -536
- hud/otel/exporters.py +366 -366
- hud/otel/instrumentation.py +97 -97
- hud/otel/processors.py +118 -118
- hud/otel/tests/__init__.py +1 -1
- hud/otel/tests/test_processors.py +197 -197
- hud/server/__init__.py +5 -5
- hud/server/context.py +114 -114
- hud/server/helper/__init__.py +5 -5
- hud/server/low_level.py +132 -132
- hud/server/server.py +170 -166
- hud/server/tests/__init__.py +3 -3
- hud/settings.py +73 -73
- hud/shared/__init__.py +5 -5
- hud/shared/exceptions.py +180 -180
- hud/shared/requests.py +264 -264
- hud/shared/tests/test_exceptions.py +157 -157
- hud/shared/tests/test_requests.py +275 -275
- hud/telemetry/__init__.py +25 -25
- hud/telemetry/instrument.py +379 -379
- hud/telemetry/job.py +309 -309
- hud/telemetry/replay.py +74 -74
- hud/telemetry/trace.py +83 -83
- hud/tools/__init__.py +33 -33
- hud/tools/base.py +365 -365
- hud/tools/bash.py +161 -161
- hud/tools/computer/__init__.py +15 -15
- hud/tools/computer/anthropic.py +437 -437
- hud/tools/computer/hud.py +376 -376
- hud/tools/computer/openai.py +295 -295
- hud/tools/computer/settings.py +82 -82
- hud/tools/edit.py +314 -314
- hud/tools/executors/__init__.py +30 -30
- hud/tools/executors/base.py +539 -539
- hud/tools/executors/pyautogui.py +621 -621
- 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 -511
- hud/tools/playwright.py +412 -412
- hud/tools/tests/__init__.py +3 -3
- hud/tools/tests/test_base.py +282 -282
- hud/tools/tests/test_bash.py +158 -158
- hud/tools/tests/test_bash_extended.py +197 -197
- hud/tools/tests/test_computer.py +425 -425
- hud/tools/tests/test_computer_actions.py +34 -34
- hud/tools/tests/test_edit.py +259 -259
- hud/tools/tests/test_init.py +27 -27
- hud/tools/tests/test_playwright_tool.py +183 -183
- hud/tools/tests/test_tools.py +145 -145
- hud/tools/tests/test_utils.py +156 -156
- hud/tools/types.py +72 -72
- hud/tools/utils.py +50 -50
- hud/types.py +136 -136
- hud/utils/__init__.py +10 -10
- hud/utils/async_utils.py +65 -65
- hud/utils/design.py +236 -168
- hud/utils/mcp.py +55 -55
- hud/utils/progress.py +149 -149
- hud/utils/telemetry.py +66 -66
- hud/utils/tests/test_async_utils.py +173 -173
- hud/utils/tests/test_init.py +17 -17
- hud/utils/tests/test_progress.py +261 -261
- hud/utils/tests/test_telemetry.py +82 -82
- hud/utils/tests/test_version.py +8 -8
- hud/version.py +7 -7
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
- hud_python-0.4.3.dist-info/RECORD +131 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
- hud/agents/art.py +0 -101
- hud_python-0.4.1.dist-info/RECORD +0 -132
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
- {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
hud/cli/tests/test_utils.py
CHANGED
|
@@ -1,388 +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__])
|
|
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__])
|