hud-python 0.4.11__py3-none-any.whl → 0.4.13__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 +114 -52
- hud/cli/build.py +121 -71
- hud/cli/debug.py +2 -2
- hud/cli/{mcp_server.py → dev.py} +101 -38
- hud/cli/eval.py +175 -90
- hud/cli/init.py +442 -64
- 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 +5 -13
- hud/clients/mcp_use.py +6 -10
- hud/server/server.py +35 -5
- 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 +12 -8
- 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.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.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.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
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
|
):
|
hud/cli/tests/test_pull.py
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import subprocess
|
|
7
|
-
from pathlib import Path
|
|
8
6
|
from unittest import mock
|
|
9
7
|
|
|
10
8
|
import pytest
|
|
@@ -30,22 +28,14 @@ class TestGetDockerManifest:
|
|
|
30
28
|
manifest_data = {
|
|
31
29
|
"schemaVersion": 2,
|
|
32
30
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
|
33
|
-
"layers": [
|
|
34
|
-
{"size": 1024},
|
|
35
|
-
{"size": 2048}
|
|
36
|
-
]
|
|
31
|
+
"layers": [{"size": 1024}, {"size": 2048}],
|
|
37
32
|
}
|
|
38
|
-
mock_run.return_value = mock.Mock(
|
|
39
|
-
returncode=0,
|
|
40
|
-
stdout=json.dumps(manifest_data)
|
|
41
|
-
)
|
|
33
|
+
mock_run.return_value = mock.Mock(returncode=0, stdout=json.dumps(manifest_data))
|
|
42
34
|
|
|
43
35
|
result = get_docker_manifest("test:latest")
|
|
44
36
|
assert result == manifest_data
|
|
45
37
|
mock_run.assert_called_once_with(
|
|
46
|
-
["docker", "manifest", "inspect", "test:latest"],
|
|
47
|
-
capture_output=True,
|
|
48
|
-
text=True
|
|
38
|
+
["docker", "manifest", "inspect", "test:latest"], capture_output=True, text=True
|
|
49
39
|
)
|
|
50
40
|
|
|
51
41
|
@mock.patch("subprocess.run")
|
|
@@ -70,14 +60,8 @@ class TestGetImageSizeFromManifest:
|
|
|
70
60
|
|
|
71
61
|
def test_get_size_v2_manifest(self):
|
|
72
62
|
"""Test getting size from v2 manifest with layers."""
|
|
73
|
-
manifest = {
|
|
74
|
-
|
|
75
|
-
{"size": 1024},
|
|
76
|
-
{"size": 2048},
|
|
77
|
-
{"size": 512}
|
|
78
|
-
]
|
|
79
|
-
}
|
|
80
|
-
|
|
63
|
+
manifest = {"layers": [{"size": 1024}, {"size": 2048}, {"size": 512}]}
|
|
64
|
+
|
|
81
65
|
size = get_image_size_from_manifest(manifest)
|
|
82
66
|
assert size == 3584 # Sum of all layers
|
|
83
67
|
|
|
@@ -86,24 +70,24 @@ class TestGetImageSizeFromManifest:
|
|
|
86
70
|
manifest = {
|
|
87
71
|
"manifests": [
|
|
88
72
|
{"size": 5120, "platform": {"os": "linux"}},
|
|
89
|
-
{"size": 4096, "platform": {"os": "windows"}}
|
|
73
|
+
{"size": 4096, "platform": {"os": "windows"}},
|
|
90
74
|
]
|
|
91
75
|
}
|
|
92
|
-
|
|
76
|
+
|
|
93
77
|
size = get_image_size_from_manifest(manifest)
|
|
94
78
|
assert size == 5120 # First manifest size
|
|
95
79
|
|
|
96
80
|
def test_get_size_empty_manifest(self):
|
|
97
81
|
"""Test getting size from empty manifest."""
|
|
98
82
|
manifest = {}
|
|
99
|
-
|
|
83
|
+
|
|
100
84
|
size = get_image_size_from_manifest(manifest)
|
|
101
85
|
assert size is None
|
|
102
86
|
|
|
103
87
|
def test_get_size_invalid_manifest(self):
|
|
104
88
|
"""Test getting size from invalid manifest."""
|
|
105
89
|
manifest = {"invalid": "data"}
|
|
106
|
-
|
|
90
|
+
|
|
107
91
|
size = get_image_size_from_manifest(manifest)
|
|
108
92
|
assert size is None
|
|
109
93
|
|
|
@@ -132,24 +116,24 @@ class TestFetchLockFromRegistry:
|
|
|
132
116
|
mock_get.return_value = mock_response
|
|
133
117
|
|
|
134
118
|
fetch_lock_from_registry("org/env")
|
|
135
|
-
|
|
136
|
-
# Check URL includes :latest
|
|
119
|
+
|
|
120
|
+
# Check URL includes :latest (URL-encoded)
|
|
137
121
|
call_args = mock_get.call_args
|
|
138
|
-
assert "org/env
|
|
122
|
+
assert "org/env%3Alatest" in call_args[0][0]
|
|
139
123
|
|
|
140
124
|
@mock.patch("hud.cli.pull.settings")
|
|
141
125
|
@mock.patch("requests.get")
|
|
142
126
|
def test_fetch_lock_with_auth(self, mock_get, mock_settings):
|
|
143
127
|
"""Test fetching with API key."""
|
|
144
128
|
mock_settings.api_key = "test-key"
|
|
145
|
-
|
|
129
|
+
|
|
146
130
|
mock_response = mock.Mock()
|
|
147
131
|
mock_response.status_code = 200
|
|
148
132
|
mock_response.json.return_value = {"test": "data"}
|
|
149
133
|
mock_get.return_value = mock_response
|
|
150
134
|
|
|
151
135
|
fetch_lock_from_registry("org/env:latest")
|
|
152
|
-
|
|
136
|
+
|
|
153
137
|
# Check auth header was set
|
|
154
138
|
call_kwargs = mock_get.call_args[1]
|
|
155
139
|
assert call_kwargs["headers"]["Authorization"] == "Bearer test-key"
|
|
@@ -205,21 +189,13 @@ class TestPullEnvironment:
|
|
|
205
189
|
mock_design = mock.Mock()
|
|
206
190
|
mock_design.console = mock.Mock()
|
|
207
191
|
mock_design_class.return_value = mock_design
|
|
208
|
-
|
|
192
|
+
|
|
209
193
|
# Create lock file
|
|
210
194
|
lock_data = {
|
|
211
195
|
"image": "test/env:latest@sha256:abc123",
|
|
212
|
-
"build": {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
},
|
|
216
|
-
"environment": {
|
|
217
|
-
"toolCount": 5,
|
|
218
|
-
"initializeMs": 1500
|
|
219
|
-
},
|
|
220
|
-
"tools": [
|
|
221
|
-
{"name": "tool1", "description": "Tool 1"}
|
|
222
|
-
]
|
|
196
|
+
"build": {"generatedAt": "2024-01-01T00:00:00Z", "hudVersion": "1.0.0"},
|
|
197
|
+
"environment": {"toolCount": 5, "initializeMs": 1500},
|
|
198
|
+
"tools": [{"name": "tool1", "description": "Tool 1"}],
|
|
223
199
|
}
|
|
224
200
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
225
201
|
lock_file.write_text(yaml.dump(lock_data))
|
|
@@ -251,12 +227,9 @@ class TestPullEnvironment:
|
|
|
251
227
|
mock_design = mock.Mock()
|
|
252
228
|
mock_design.console = mock.Mock()
|
|
253
229
|
mock_design_class.return_value = mock_design
|
|
254
|
-
|
|
230
|
+
|
|
255
231
|
# Mock registry response
|
|
256
|
-
lock_data = {
|
|
257
|
-
"image": "docker.io/org/env:latest@sha256:def456",
|
|
258
|
-
"tools": []
|
|
259
|
-
}
|
|
232
|
+
lock_data = {"image": "docker.io/org/env:latest@sha256:def456", "tools": []}
|
|
260
233
|
mock_fetch.return_value = lock_data
|
|
261
234
|
|
|
262
235
|
# Mock docker pull
|
|
@@ -281,20 +254,20 @@ class TestPullEnvironment:
|
|
|
281
254
|
@mock.patch("hud.cli.pull.get_docker_manifest")
|
|
282
255
|
@mock.patch("hud.cli.pull.fetch_lock_from_registry")
|
|
283
256
|
@mock.patch("subprocess.Popen")
|
|
284
|
-
def test_pull_docker_image_direct(
|
|
257
|
+
def test_pull_docker_image_direct(
|
|
258
|
+
self, mock_popen, mock_fetch, mock_manifest, mock_design_class
|
|
259
|
+
):
|
|
285
260
|
"""Test pulling Docker image directly."""
|
|
286
261
|
# Create mock design instance
|
|
287
262
|
mock_design = mock.Mock()
|
|
288
263
|
mock_design.console = mock.Mock()
|
|
289
264
|
mock_design_class.return_value = mock_design
|
|
290
|
-
|
|
265
|
+
|
|
291
266
|
# Mock no registry data
|
|
292
267
|
mock_fetch.return_value = None
|
|
293
268
|
|
|
294
269
|
# Mock manifest
|
|
295
|
-
mock_manifest.return_value = {
|
|
296
|
-
"layers": [{"size": 1024}]
|
|
297
|
-
}
|
|
270
|
+
mock_manifest.return_value = {"layers": [{"size": 1024}]}
|
|
298
271
|
|
|
299
272
|
# Mock docker pull
|
|
300
273
|
mock_process = mock.Mock()
|
|
@@ -318,7 +291,7 @@ class TestPullEnvironment:
|
|
|
318
291
|
mock_design = mock.Mock()
|
|
319
292
|
mock_design.console = mock.Mock()
|
|
320
293
|
mock_design_class.return_value = mock_design
|
|
321
|
-
|
|
294
|
+
|
|
322
295
|
# Should not actually pull
|
|
323
296
|
pull_environment("test:latest", verify_only=True)
|
|
324
297
|
|
|
@@ -333,7 +306,7 @@ class TestPullEnvironment:
|
|
|
333
306
|
mock_design = mock.Mock()
|
|
334
307
|
mock_design.console = mock.Mock()
|
|
335
308
|
mock_design_class.return_value = mock_design
|
|
336
|
-
|
|
309
|
+
|
|
337
310
|
# Mock docker pull failure
|
|
338
311
|
mock_process = mock.Mock()
|
|
339
312
|
mock_process.stdout = ["Error: manifest unknown\n"]
|
|
@@ -355,7 +328,7 @@ class TestPullEnvironment:
|
|
|
355
328
|
mock_design = mock.Mock()
|
|
356
329
|
mock_design.console = mock.Mock()
|
|
357
330
|
mock_design_class.return_value = mock_design
|
|
358
|
-
|
|
331
|
+
|
|
359
332
|
mock_confirm.return_value = False
|
|
360
333
|
|
|
361
334
|
with pytest.raises(typer.Exit) as exc_info:
|
|
@@ -371,7 +344,7 @@ class TestPullEnvironment:
|
|
|
371
344
|
mock_design = mock.Mock()
|
|
372
345
|
mock_design.console = mock.Mock()
|
|
373
346
|
mock_design_class.return_value = mock_design
|
|
374
|
-
|
|
347
|
+
|
|
375
348
|
with pytest.raises(typer.Exit):
|
|
376
349
|
pull_environment("nonexistent.yaml")
|
|
377
350
|
|
|
@@ -392,9 +365,5 @@ class TestPullCommand:
|
|
|
392
365
|
# Just test it doesn't crash with explicit values
|
|
393
366
|
with mock.patch("hud.cli.pull.pull_environment"):
|
|
394
367
|
pull_command(
|
|
395
|
-
"org/env:v1.0",
|
|
396
|
-
lock_file="lock.yaml",
|
|
397
|
-
yes=True,
|
|
398
|
-
verify_only=True,
|
|
399
|
-
verbose=True
|
|
368
|
+
"org/env:v1.0", lock_file="lock.yaml", yes=True, verify_only=True, verbose=True
|
|
400
369
|
)
|