hud-python 0.4.10__py3-none-any.whl → 0.4.12__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/__main__.py +8 -0
- hud/agents/base.py +7 -8
- hud/agents/langchain.py +2 -2
- hud/agents/tests/test_openai.py +3 -1
- hud/cli/__init__.py +106 -51
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +60 -25
- hud/cli/eval.py +148 -68
- hud/cli/init.py +0 -1
- hud/cli/list_func.py +72 -71
- hud/cli/pull.py +1 -2
- hud/cli/push.py +35 -23
- hud/cli/remove.py +35 -41
- hud/cli/tests/test_analyze.py +2 -1
- hud/cli/tests/test_analyze_metadata.py +42 -49
- hud/cli/tests/test_build.py +28 -52
- hud/cli/tests/test_cursor.py +1 -1
- hud/cli/tests/test_debug.py +1 -1
- hud/cli/tests/test_list_func.py +75 -64
- hud/cli/tests/test_main_module.py +30 -0
- hud/cli/tests/test_mcp_server.py +3 -3
- hud/cli/tests/test_pull.py +30 -61
- hud/cli/tests/test_push.py +70 -89
- hud/cli/tests/test_registry.py +36 -38
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/utils/__init__.py +1 -0
- hud/cli/{docker_utils.py → utils/docker.py} +36 -0
- hud/cli/{env_utils.py → utils/environment.py} +7 -7
- hud/cli/{interactive.py → utils/interactive.py} +91 -19
- hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
- hud/cli/{registry.py → utils/registry.py} +28 -30
- hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
- hud/cli/utils/runner.py +134 -0
- hud/cli/utils/server.py +250 -0
- hud/clients/base.py +1 -1
- hud/clients/fastmcp.py +7 -5
- hud/clients/mcp_use.py +8 -6
- hud/server/server.py +34 -4
- hud/shared/exceptions.py +11 -0
- hud/shared/tests/test_exceptions.py +22 -0
- hud/telemetry/tests/__init__.py +0 -0
- hud/telemetry/tests/test_replay.py +40 -0
- hud/telemetry/tests/test_trace.py +63 -0
- hud/tools/base.py +20 -3
- hud/tools/computer/hud.py +15 -6
- hud/tools/executors/tests/test_base_executor.py +27 -0
- hud/tools/response.py +15 -4
- hud/tools/tests/test_response.py +60 -0
- hud/tools/tests/test_tools_init.py +49 -0
- hud/utils/design.py +19 -8
- hud/utils/mcp.py +17 -5
- hud/utils/tests/test_mcp.py +112 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/RECORD +62 -52
- hud/cli/runner.py +0 -160
- /hud/cli/{cursor.py → utils/cursor.py} +0 -0
- /hud/cli/{utils.py → utils/logging.py} +0 -0
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
hud/cli/tests/test_build.py
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import subprocess
|
|
6
|
-
import tempfile
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from pathlib import Path
|
|
9
6
|
from unittest import mock
|
|
10
7
|
|
|
11
8
|
import pytest
|
|
@@ -81,11 +78,7 @@ class TestGetExistingVersion:
|
|
|
81
78
|
|
|
82
79
|
def test_get_version_from_lock(self, tmp_path):
|
|
83
80
|
"""Test extracting version from lock file."""
|
|
84
|
-
lock_data = {
|
|
85
|
-
"build": {
|
|
86
|
-
"version": "1.2.3"
|
|
87
|
-
}
|
|
88
|
-
}
|
|
81
|
+
lock_data = {"build": {"version": "1.2.3"}}
|
|
89
82
|
lock_path = tmp_path / "hud.lock.yaml"
|
|
90
83
|
lock_path.write_text(yaml.dump(lock_data))
|
|
91
84
|
|
|
@@ -119,8 +112,7 @@ class TestGetDockerImageDigest:
|
|
|
119
112
|
"""Test successfully getting image digest."""
|
|
120
113
|
# Note: The function expects to parse a list from the string representation
|
|
121
114
|
mock_run.return_value = mock.Mock(
|
|
122
|
-
stdout="['docker.io/library/test@sha256:abc123']",
|
|
123
|
-
returncode=0
|
|
115
|
+
stdout="['docker.io/library/test@sha256:abc123']", returncode=0
|
|
124
116
|
)
|
|
125
117
|
|
|
126
118
|
result = get_docker_image_digest("test:latest")
|
|
@@ -149,10 +141,7 @@ class TestGetDockerImageId:
|
|
|
149
141
|
@mock.patch("subprocess.run")
|
|
150
142
|
def test_get_id_success(self, mock_run):
|
|
151
143
|
"""Test successfully getting image ID."""
|
|
152
|
-
mock_run.return_value = mock.Mock(
|
|
153
|
-
stdout="sha256:abc123def456",
|
|
154
|
-
returncode=0
|
|
155
|
-
)
|
|
144
|
+
mock_run.return_value = mock.Mock(stdout="sha256:abc123def456", returncode=0)
|
|
156
145
|
|
|
157
146
|
result = get_docker_image_id("test:latest")
|
|
158
147
|
assert result == "sha256:abc123def456"
|
|
@@ -223,13 +212,13 @@ class TestAnalyzeMcpEnvironment:
|
|
|
223
212
|
# Setup mock client
|
|
224
213
|
mock_client = mock.AsyncMock()
|
|
225
214
|
mock_client_class.return_value = mock_client
|
|
226
|
-
|
|
215
|
+
|
|
227
216
|
# Mock tool
|
|
228
217
|
mock_tool = mock.Mock()
|
|
229
218
|
mock_tool.name = "test_tool"
|
|
230
219
|
mock_tool.description = "Test tool"
|
|
231
220
|
mock_tool.inputSchema = {"type": "object"}
|
|
232
|
-
|
|
221
|
+
|
|
233
222
|
mock_client.list_tools.return_value = [mock_tool]
|
|
234
223
|
|
|
235
224
|
result = await analyze_mcp_environment("test:latest")
|
|
@@ -264,7 +253,7 @@ class TestAnalyzeMcpEnvironment:
|
|
|
264
253
|
|
|
265
254
|
# Just test that it runs without error in verbose mode
|
|
266
255
|
result = await analyze_mcp_environment("test:latest", verbose=True)
|
|
267
|
-
|
|
256
|
+
|
|
268
257
|
assert result["success"] is True
|
|
269
258
|
assert "initializeMs" in result
|
|
270
259
|
|
|
@@ -272,35 +261,31 @@ class TestAnalyzeMcpEnvironment:
|
|
|
272
261
|
class TestBuildDockerImage:
|
|
273
262
|
"""Test building Docker images."""
|
|
274
263
|
|
|
275
|
-
@mock.patch("subprocess.
|
|
276
|
-
def test_build_success(self,
|
|
264
|
+
@mock.patch("subprocess.run")
|
|
265
|
+
def test_build_success(self, mock_run, tmp_path):
|
|
277
266
|
"""Test successful Docker build."""
|
|
278
267
|
# Create Dockerfile
|
|
279
268
|
dockerfile = tmp_path / "Dockerfile"
|
|
280
269
|
dockerfile.write_text("FROM python:3.11")
|
|
281
270
|
|
|
282
271
|
# Mock successful process
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
mock_process.returncode = 0
|
|
287
|
-
mock_popen.return_value = mock_process
|
|
272
|
+
mock_result = mock.Mock()
|
|
273
|
+
mock_result.returncode = 0
|
|
274
|
+
mock_run.return_value = mock_result
|
|
288
275
|
|
|
289
276
|
result = build_docker_image(tmp_path, "test:latest")
|
|
290
277
|
assert result is True
|
|
291
278
|
|
|
292
|
-
@mock.patch("subprocess.
|
|
293
|
-
def test_build_failure(self,
|
|
279
|
+
@mock.patch("subprocess.run")
|
|
280
|
+
def test_build_failure(self, mock_run, tmp_path):
|
|
294
281
|
"""Test failed Docker build."""
|
|
295
282
|
dockerfile = tmp_path / "Dockerfile"
|
|
296
283
|
dockerfile.write_text("FROM python:3.11")
|
|
297
284
|
|
|
298
285
|
# Mock failed process
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
mock_process.returncode = 1
|
|
303
|
-
mock_popen.return_value = mock_process
|
|
286
|
+
mock_result = mock.Mock()
|
|
287
|
+
mock_result.returncode = 1
|
|
288
|
+
mock_run.return_value = mock_result
|
|
304
289
|
|
|
305
290
|
result = build_docker_image(tmp_path, "test:latest")
|
|
306
291
|
assert result is False
|
|
@@ -310,22 +295,20 @@ class TestBuildDockerImage:
|
|
|
310
295
|
result = build_docker_image(tmp_path, "test:latest")
|
|
311
296
|
assert result is False
|
|
312
297
|
|
|
313
|
-
@mock.patch("subprocess.
|
|
314
|
-
def test_build_with_no_cache(self,
|
|
298
|
+
@mock.patch("subprocess.run")
|
|
299
|
+
def test_build_with_no_cache(self, mock_run, tmp_path):
|
|
315
300
|
"""Test build with --no-cache flag."""
|
|
316
301
|
dockerfile = tmp_path / "Dockerfile"
|
|
317
302
|
dockerfile.write_text("FROM python:3.11")
|
|
318
303
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
mock_process.returncode = 0
|
|
323
|
-
mock_popen.return_value = mock_process
|
|
304
|
+
mock_result = mock.Mock()
|
|
305
|
+
mock_result.returncode = 0
|
|
306
|
+
mock_run.return_value = mock_result
|
|
324
307
|
|
|
325
308
|
build_docker_image(tmp_path, "test:latest", no_cache=True)
|
|
326
309
|
|
|
327
310
|
# Check that --no-cache was included
|
|
328
|
-
call_args =
|
|
311
|
+
call_args = mock_run.call_args[0][0]
|
|
329
312
|
assert "--no-cache" in call_args
|
|
330
313
|
|
|
331
314
|
|
|
@@ -336,12 +319,10 @@ class TestBuildEnvironment:
|
|
|
336
319
|
@mock.patch("hud.cli.build.analyze_mcp_environment")
|
|
337
320
|
@mock.patch("hud.cli.build.save_to_registry")
|
|
338
321
|
@mock.patch("hud.cli.build.get_docker_image_id")
|
|
339
|
-
@mock.patch("subprocess.Popen")
|
|
340
322
|
@mock.patch("subprocess.run")
|
|
341
323
|
def test_build_environment_success(
|
|
342
324
|
self,
|
|
343
325
|
mock_run,
|
|
344
|
-
mock_popen,
|
|
345
326
|
mock_get_id,
|
|
346
327
|
mock_save_registry,
|
|
347
328
|
mock_analyze,
|
|
@@ -352,14 +333,14 @@ class TestBuildEnvironment:
|
|
|
352
333
|
# Setup directory structure
|
|
353
334
|
env_dir = tmp_path / "test-env"
|
|
354
335
|
env_dir.mkdir()
|
|
355
|
-
|
|
336
|
+
|
|
356
337
|
# Create pyproject.toml
|
|
357
338
|
pyproject = env_dir / "pyproject.toml"
|
|
358
339
|
pyproject.write_text("""
|
|
359
340
|
[tool.hud]
|
|
360
341
|
image = "test/env:dev"
|
|
361
342
|
""")
|
|
362
|
-
|
|
343
|
+
|
|
363
344
|
# Create Dockerfile
|
|
364
345
|
dockerfile = env_dir / "Dockerfile"
|
|
365
346
|
dockerfile.write_text("""
|
|
@@ -379,16 +360,11 @@ ENV API_KEY
|
|
|
379
360
|
],
|
|
380
361
|
}
|
|
381
362
|
mock_get_id.return_value = "sha256:abc123"
|
|
382
|
-
|
|
363
|
+
|
|
383
364
|
# Mock final rebuild
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
mock_stdout.read.return_value = ""
|
|
388
|
-
mock_process.stdout = mock_stdout
|
|
389
|
-
mock_process.wait.return_value = None
|
|
390
|
-
mock_process.returncode = 0
|
|
391
|
-
mock_popen.return_value = mock_process
|
|
365
|
+
mock_result = mock.Mock()
|
|
366
|
+
mock_result.returncode = 0
|
|
367
|
+
mock_run.return_value = mock_result
|
|
392
368
|
|
|
393
369
|
# Run build
|
|
394
370
|
build_environment(str(env_dir), "test/env:latest")
|
hud/cli/tests/test_cursor.py
CHANGED
|
@@ -9,7 +9,7 @@ from unittest.mock import mock_open, patch
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from hud.cli.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
|
|
12
|
+
from hud.cli.utils.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class TestParseCursorConfig:
|
hud/cli/tests/test_debug.py
CHANGED
hud/cli/tests/test_list_func.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import tempfile
|
|
7
6
|
import time
|
|
8
7
|
from datetime import datetime, timedelta
|
|
9
8
|
from pathlib import Path
|
|
@@ -12,7 +11,7 @@ from unittest import mock
|
|
|
12
11
|
import pytest
|
|
13
12
|
import yaml
|
|
14
13
|
|
|
15
|
-
from hud.cli.list_func import format_timestamp,
|
|
14
|
+
from hud.cli.list_func import format_timestamp, list_command, list_environments
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class TestFormatTimestamp:
|
|
@@ -26,11 +25,11 @@ class TestFormatTimestamp:
|
|
|
26
25
|
def test_format_timestamp_minutes(self):
|
|
27
26
|
"""Test formatting for timestamps in minutes."""
|
|
28
27
|
now = datetime.now()
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
# 5 minutes ago
|
|
31
30
|
timestamp = (now - timedelta(minutes=5)).timestamp()
|
|
32
31
|
assert format_timestamp(timestamp) == "5m ago"
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
# 45 minutes ago
|
|
35
34
|
timestamp = (now - timedelta(minutes=45)).timestamp()
|
|
36
35
|
assert format_timestamp(timestamp) == "45m ago"
|
|
@@ -38,11 +37,11 @@ class TestFormatTimestamp:
|
|
|
38
37
|
def test_format_timestamp_hours(self):
|
|
39
38
|
"""Test formatting for timestamps in hours."""
|
|
40
39
|
now = datetime.now()
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
# 2 hours ago
|
|
43
42
|
timestamp = (now - timedelta(hours=2)).timestamp()
|
|
44
43
|
assert format_timestamp(timestamp) == "2h ago"
|
|
45
|
-
|
|
44
|
+
|
|
46
45
|
# 23 hours ago
|
|
47
46
|
timestamp = (now - timedelta(hours=23)).timestamp()
|
|
48
47
|
assert format_timestamp(timestamp) == "23h ago"
|
|
@@ -50,11 +49,11 @@ class TestFormatTimestamp:
|
|
|
50
49
|
def test_format_timestamp_days(self):
|
|
51
50
|
"""Test formatting for timestamps in days."""
|
|
52
51
|
now = datetime.now()
|
|
53
|
-
|
|
52
|
+
|
|
54
53
|
# 3 days ago
|
|
55
54
|
timestamp = (now - timedelta(days=3)).timestamp()
|
|
56
55
|
assert format_timestamp(timestamp) == "3d ago"
|
|
57
|
-
|
|
56
|
+
|
|
58
57
|
# 29 days ago
|
|
59
58
|
timestamp = (now - timedelta(days=29)).timestamp()
|
|
60
59
|
assert format_timestamp(timestamp) == "29d ago"
|
|
@@ -62,11 +61,11 @@ class TestFormatTimestamp:
|
|
|
62
61
|
def test_format_timestamp_months(self):
|
|
63
62
|
"""Test formatting for timestamps in months."""
|
|
64
63
|
now = datetime.now()
|
|
65
|
-
|
|
64
|
+
|
|
66
65
|
# 2 months ago
|
|
67
66
|
timestamp = (now - timedelta(days=60)).timestamp()
|
|
68
67
|
assert format_timestamp(timestamp) == "2mo ago"
|
|
69
|
-
|
|
68
|
+
|
|
70
69
|
# 11 months ago
|
|
71
70
|
timestamp = (now - timedelta(days=335)).timestamp()
|
|
72
71
|
assert format_timestamp(timestamp) == "11mo ago"
|
|
@@ -74,11 +73,11 @@ class TestFormatTimestamp:
|
|
|
74
73
|
def test_format_timestamp_years(self):
|
|
75
74
|
"""Test formatting for timestamps in years."""
|
|
76
75
|
now = datetime.now()
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
# 1 year ago
|
|
79
78
|
timestamp = (now - timedelta(days=400)).timestamp()
|
|
80
79
|
assert format_timestamp(timestamp) == "1y ago"
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
# 3 years ago
|
|
83
82
|
timestamp = (now - timedelta(days=1100)).timestamp()
|
|
84
83
|
assert format_timestamp(timestamp) == "3y ago"
|
|
@@ -96,52 +95,59 @@ class TestListEnvironments:
|
|
|
96
95
|
"""Create a mock registry directory with sample environments."""
|
|
97
96
|
registry_dir = tmp_path / ".hud" / "envs"
|
|
98
97
|
registry_dir.mkdir(parents=True)
|
|
99
|
-
|
|
98
|
+
|
|
100
99
|
# Create sample environments (use underscore instead of colon for Windows compatibility)
|
|
101
100
|
env1_dir = registry_dir / "sha256_abc123"
|
|
102
101
|
env1_dir.mkdir()
|
|
103
102
|
lock1 = env1_dir / "hud.lock.yaml"
|
|
104
|
-
lock1.write_text(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
lock1.write_text(
|
|
104
|
+
yaml.dump(
|
|
105
|
+
{
|
|
106
|
+
"image": "test/env1:latest",
|
|
107
|
+
"metadata": {
|
|
108
|
+
"description": "Test environment 1",
|
|
109
|
+
"tools": ["tool1", "tool2", "tool3"],
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
)
|
|
111
114
|
# Set modification time to 1 hour ago
|
|
112
|
-
old_time = time.time() - 3600
|
|
113
115
|
lock1.touch()
|
|
114
|
-
|
|
116
|
+
|
|
115
117
|
env2_dir = registry_dir / "sha256_def456"
|
|
116
118
|
env2_dir.mkdir()
|
|
117
119
|
lock2 = env2_dir / "hud.lock.yaml"
|
|
118
|
-
lock2.write_text(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
lock2.write_text(
|
|
121
|
+
yaml.dump(
|
|
122
|
+
{
|
|
123
|
+
"image": "test/env2:v1.0",
|
|
124
|
+
"metadata": {
|
|
125
|
+
"description": "Test environment 2 with a much longer description that should be truncated", # noqa: E501
|
|
126
|
+
"tools": ["tool1"],
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
126
132
|
return registry_dir
|
|
127
133
|
|
|
128
134
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
129
135
|
def test_list_no_registry(self, mock_get_registry):
|
|
130
136
|
"""Test listing when registry doesn't exist."""
|
|
131
137
|
mock_get_registry.return_value = Path("/nonexistent")
|
|
132
|
-
|
|
138
|
+
|
|
133
139
|
# Just test it runs without error
|
|
134
140
|
list_environments()
|
|
135
|
-
|
|
141
|
+
|
|
136
142
|
# The design messages will be printed but we don't need to test them
|
|
137
143
|
|
|
138
144
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
139
145
|
def test_list_json_no_registry(self, mock_get_registry, capsys):
|
|
140
146
|
"""Test JSON output when registry doesn't exist."""
|
|
141
147
|
mock_get_registry.return_value = Path("/nonexistent")
|
|
142
|
-
|
|
148
|
+
|
|
143
149
|
list_environments(json_output=True)
|
|
144
|
-
|
|
150
|
+
|
|
145
151
|
captured = capsys.readouterr()
|
|
146
152
|
assert captured.out.strip() == "[]"
|
|
147
153
|
|
|
@@ -150,55 +156,59 @@ class TestListEnvironments:
|
|
|
150
156
|
def test_list_environments_basic(self, mock_list_entries, mock_get_registry, mock_registry_dir):
|
|
151
157
|
"""Test basic environment listing."""
|
|
152
158
|
mock_get_registry.return_value = mock_registry_dir
|
|
153
|
-
|
|
159
|
+
|
|
154
160
|
# Mock registry entries
|
|
155
161
|
entries = [
|
|
156
162
|
("sha256_abc123", mock_registry_dir / "sha256_abc123" / "hud.lock.yaml"),
|
|
157
163
|
("sha256_def456", mock_registry_dir / "sha256_def456" / "hud.lock.yaml"),
|
|
158
164
|
]
|
|
159
165
|
mock_list_entries.return_value = entries
|
|
160
|
-
|
|
166
|
+
|
|
161
167
|
# Just test it runs without error
|
|
162
168
|
list_environments()
|
|
163
169
|
|
|
164
170
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
165
171
|
@mock.patch("hud.cli.list_func.list_registry_entries")
|
|
166
|
-
def test_list_environments_json(
|
|
172
|
+
def test_list_environments_json(
|
|
173
|
+
self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys
|
|
174
|
+
):
|
|
167
175
|
"""Test JSON output format."""
|
|
168
176
|
mock_get_registry.return_value = mock_registry_dir
|
|
169
|
-
|
|
177
|
+
|
|
170
178
|
# Mock registry entries
|
|
171
179
|
entries = [
|
|
172
180
|
("sha256_abc123", mock_registry_dir / "sha256_abc123" / "hud.lock.yaml"),
|
|
173
181
|
("sha256_def456", mock_registry_dir / "sha256_def456" / "hud.lock.yaml"),
|
|
174
182
|
]
|
|
175
183
|
mock_list_entries.return_value = entries
|
|
176
|
-
|
|
184
|
+
|
|
177
185
|
list_environments(json_output=True)
|
|
178
|
-
|
|
186
|
+
|
|
179
187
|
captured = capsys.readouterr()
|
|
180
188
|
data = json.loads(captured.out)
|
|
181
|
-
|
|
189
|
+
|
|
182
190
|
assert len(data) == 2
|
|
183
191
|
# Check we have the expected structure
|
|
184
192
|
assert all(key in data[0] for key in ["name", "tag", "tools_count", "digest"])
|
|
185
193
|
|
|
186
194
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
187
195
|
@mock.patch("hud.cli.list_func.list_registry_entries")
|
|
188
|
-
def test_list_environments_filter(
|
|
196
|
+
def test_list_environments_filter(
|
|
197
|
+
self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys
|
|
198
|
+
):
|
|
189
199
|
"""Test filtering environments by name."""
|
|
190
200
|
mock_get_registry.return_value = mock_registry_dir
|
|
191
|
-
|
|
201
|
+
|
|
192
202
|
# Mock registry entries
|
|
193
203
|
entries = [
|
|
194
204
|
("sha256_abc123", mock_registry_dir / "sha256_abc123" / "hud.lock.yaml"),
|
|
195
205
|
("sha256_def456", mock_registry_dir / "sha256_def456" / "hud.lock.yaml"),
|
|
196
206
|
]
|
|
197
207
|
mock_list_entries.return_value = entries
|
|
198
|
-
|
|
208
|
+
|
|
199
209
|
# Filter for env1 and check JSON output
|
|
200
210
|
list_environments(filter_name="env1", json_output=True)
|
|
201
|
-
|
|
211
|
+
|
|
202
212
|
captured = capsys.readouterr()
|
|
203
213
|
data = json.loads(captured.out)
|
|
204
214
|
# Should only have env1
|
|
@@ -207,31 +217,35 @@ class TestListEnvironments:
|
|
|
207
217
|
|
|
208
218
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
209
219
|
@mock.patch("hud.cli.list_func.list_registry_entries")
|
|
210
|
-
def test_list_environments_verbose(
|
|
220
|
+
def test_list_environments_verbose(
|
|
221
|
+
self, mock_list_entries, mock_get_registry, mock_registry_dir
|
|
222
|
+
):
|
|
211
223
|
"""Test verbose output."""
|
|
212
224
|
mock_get_registry.return_value = mock_registry_dir
|
|
213
|
-
|
|
225
|
+
|
|
214
226
|
# Mock registry entries
|
|
215
227
|
entries = [
|
|
216
228
|
("sha256_abc123", mock_registry_dir / "sha256_abc123" / "hud.lock.yaml"),
|
|
217
229
|
]
|
|
218
230
|
mock_list_entries.return_value = entries
|
|
219
|
-
|
|
231
|
+
|
|
220
232
|
# Just test it runs in verbose mode
|
|
221
233
|
list_environments(verbose=True)
|
|
222
234
|
|
|
223
235
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
224
236
|
@mock.patch("hud.cli.list_func.list_registry_entries")
|
|
225
|
-
def test_list_environments_with_errors(
|
|
237
|
+
def test_list_environments_with_errors(
|
|
238
|
+
self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys
|
|
239
|
+
):
|
|
226
240
|
"""Test handling of corrupted lock files."""
|
|
227
241
|
mock_get_registry.return_value = mock_registry_dir
|
|
228
|
-
|
|
242
|
+
|
|
229
243
|
# Create a bad lock file
|
|
230
244
|
bad_dir = mock_registry_dir / "sha256_bad"
|
|
231
245
|
bad_dir.mkdir()
|
|
232
246
|
bad_lock = bad_dir / "hud.lock.yaml"
|
|
233
247
|
bad_lock.write_text("invalid: yaml: content:")
|
|
234
|
-
|
|
248
|
+
|
|
235
249
|
# Mock registry entries including the bad one
|
|
236
250
|
entries = [
|
|
237
251
|
("sha256_bad", bad_lock),
|
|
@@ -239,10 +253,10 @@ class TestListEnvironments:
|
|
|
239
253
|
("sha256_def456", mock_registry_dir / "sha256_def456" / "hud.lock.yaml"),
|
|
240
254
|
]
|
|
241
255
|
mock_list_entries.return_value = entries
|
|
242
|
-
|
|
256
|
+
|
|
243
257
|
# Should handle error gracefully in verbose mode
|
|
244
258
|
list_environments(verbose=True, json_output=True)
|
|
245
|
-
|
|
259
|
+
|
|
246
260
|
captured = capsys.readouterr()
|
|
247
261
|
data = json.loads(captured.out)
|
|
248
262
|
# Should still list the valid environments
|
|
@@ -250,20 +264,22 @@ class TestListEnvironments:
|
|
|
250
264
|
|
|
251
265
|
@mock.patch("hud.cli.list_func.get_registry_dir")
|
|
252
266
|
@mock.patch("hud.cli.list_func.list_registry_entries")
|
|
253
|
-
def test_list_environments_no_matches(
|
|
267
|
+
def test_list_environments_no_matches(
|
|
268
|
+
self, mock_list_entries, mock_get_registry, mock_registry_dir
|
|
269
|
+
):
|
|
254
270
|
"""Test when no environments match the filter."""
|
|
255
271
|
mock_get_registry.return_value = mock_registry_dir
|
|
256
|
-
|
|
272
|
+
|
|
257
273
|
# Mock registry entries
|
|
258
274
|
entries = [
|
|
259
275
|
("sha256_abc123", mock_registry_dir / "sha256_abc123" / "hud.lock.yaml"),
|
|
260
276
|
("sha256_def456", mock_registry_dir / "sha256_def456" / "hud.lock.yaml"),
|
|
261
277
|
]
|
|
262
278
|
mock_list_entries.return_value = entries
|
|
263
|
-
|
|
264
|
-
# Filter for non-existent env
|
|
279
|
+
|
|
280
|
+
# Filter for non-existent env
|
|
265
281
|
list_environments(filter_name="nonexistent")
|
|
266
|
-
|
|
282
|
+
|
|
267
283
|
# Just test it runs without error
|
|
268
284
|
|
|
269
285
|
|
|
@@ -280,9 +296,4 @@ class TestListCommand:
|
|
|
280
296
|
def test_list_command_with_options(self):
|
|
281
297
|
"""Test list command with options runs without error."""
|
|
282
298
|
# Just test it doesn't crash with explicit values
|
|
283
|
-
list_command(
|
|
284
|
-
filter_name="test",
|
|
285
|
-
json_output=True,
|
|
286
|
-
show_all=True,
|
|
287
|
-
verbose=True
|
|
288
|
-
)
|
|
299
|
+
list_command(filter_name="test", json_output=True, show_all=True, verbose=True)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Tests for hud.cli.__main__ module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestMainModule:
|
|
10
|
+
"""Tests for the CLI __main__ module."""
|
|
11
|
+
|
|
12
|
+
def test_main_module_imports_correctly(self):
|
|
13
|
+
"""Test that __main__.py imports correctly."""
|
|
14
|
+
# Simply importing the module should work without errors
|
|
15
|
+
import hud.cli.__main__
|
|
16
|
+
|
|
17
|
+
# Verify the module has the expected attributes
|
|
18
|
+
assert hasattr(hud.cli.__main__, "main")
|
|
19
|
+
|
|
20
|
+
def test_main_module_executes(self):
|
|
21
|
+
"""Test that running the module as main executes correctly."""
|
|
22
|
+
# Use subprocess to run the module as __main__ and check it doesn't crash
|
|
23
|
+
# We expect it to show help/error since we're not providing arguments
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
[sys.executable, "-m", "hud.cli"], capture_output=True, text=True, timeout=10
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Should exit with an error code but not crash
|
|
29
|
+
# (The actual main function will show help or error for missing args)
|
|
30
|
+
assert result.returncode != 0 # CLI should exit with error for no args
|
hud/cli/tests/test_mcp_server.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for hud.cli.
|
|
1
|
+
"""Tests for hud.cli.dev module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ from unittest.mock import MagicMock, patch
|
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
|
-
from hud.cli.
|
|
10
|
+
from hud.cli.dev import (
|
|
11
11
|
create_proxy_server,
|
|
12
12
|
get_docker_cmd,
|
|
13
13
|
get_image_name,
|
|
@@ -119,7 +119,7 @@ class TestRunMCPDevServer:
|
|
|
119
119
|
import click
|
|
120
120
|
|
|
121
121
|
with (
|
|
122
|
-
patch("hud.cli.
|
|
122
|
+
patch("hud.cli.dev.image_exists", return_value=False),
|
|
123
123
|
patch("click.confirm", return_value=False),
|
|
124
124
|
pytest.raises(click.Abort),
|
|
125
125
|
):
|