hud-python 0.4.11__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 +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.12.dist-info}/METADATA +14 -10
- {hud_python-0.4.11.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.11.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
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
|
)
|
hud/cli/tests/test_push.py
CHANGED
|
@@ -5,12 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import subprocess
|
|
8
|
-
from datetime import datetime
|
|
9
|
-
from pathlib import Path
|
|
10
8
|
from unittest import mock
|
|
11
9
|
|
|
12
10
|
import pytest
|
|
13
|
-
import requests
|
|
14
11
|
import typer
|
|
15
12
|
import yaml
|
|
16
13
|
|
|
@@ -30,7 +27,7 @@ class TestGetDockerUsername:
|
|
|
30
27
|
# Create mock Docker config
|
|
31
28
|
docker_dir = tmp_path / ".docker"
|
|
32
29
|
docker_dir.mkdir()
|
|
33
|
-
|
|
30
|
+
|
|
34
31
|
config_file = docker_dir / "config.json"
|
|
35
32
|
config = {
|
|
36
33
|
"auths": {
|
|
@@ -40,37 +37,31 @@ class TestGetDockerUsername:
|
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
config_file.write_text(json.dumps(config))
|
|
43
|
-
|
|
40
|
+
|
|
44
41
|
with mock.patch("pathlib.Path.home", return_value=tmp_path):
|
|
45
42
|
username = get_docker_username()
|
|
46
|
-
|
|
43
|
+
|
|
47
44
|
assert username == "testuser"
|
|
48
45
|
|
|
49
46
|
def test_get_username_no_config(self, tmp_path):
|
|
50
47
|
"""Test when no Docker config exists."""
|
|
51
48
|
with mock.patch("pathlib.Path.home", return_value=tmp_path):
|
|
52
49
|
username = get_docker_username()
|
|
53
|
-
|
|
50
|
+
|
|
54
51
|
assert username is None
|
|
55
52
|
|
|
56
53
|
def test_get_username_token_auth(self, tmp_path):
|
|
57
54
|
"""Test skipping token-based auth."""
|
|
58
55
|
docker_dir = tmp_path / ".docker"
|
|
59
56
|
docker_dir.mkdir()
|
|
60
|
-
|
|
57
|
+
|
|
61
58
|
config_file = docker_dir / "config.json"
|
|
62
|
-
config = {
|
|
63
|
-
"auths": {
|
|
64
|
-
"docker.io": {
|
|
65
|
-
"auth": base64.b64encode(b"token:xyz").decode()
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
59
|
+
config = {"auths": {"docker.io": {"auth": base64.b64encode(b"token:xyz").decode()}}}
|
|
69
60
|
config_file.write_text(json.dumps(config))
|
|
70
|
-
|
|
61
|
+
|
|
71
62
|
with mock.patch("pathlib.Path.home", return_value=tmp_path):
|
|
72
63
|
username = get_docker_username()
|
|
73
|
-
|
|
64
|
+
|
|
74
65
|
assert username is None
|
|
75
66
|
|
|
76
67
|
@mock.patch("subprocess.run")
|
|
@@ -78,20 +69,20 @@ class TestGetDockerUsername:
|
|
|
78
69
|
"""Test getting username from credential helper."""
|
|
79
70
|
docker_dir = tmp_path / ".docker"
|
|
80
71
|
docker_dir.mkdir()
|
|
81
|
-
|
|
72
|
+
|
|
82
73
|
config_file = docker_dir / "config.json"
|
|
83
74
|
config = {"credsStore": "desktop"}
|
|
84
75
|
config_file.write_text(json.dumps(config))
|
|
85
|
-
|
|
76
|
+
|
|
86
77
|
# Mock credential helper calls
|
|
87
78
|
mock_run.side_effect = [
|
|
88
79
|
mock.Mock(returncode=0, stdout='{"https://index.docker.io/v1/": "creds"}'),
|
|
89
|
-
mock.Mock(returncode=0, stdout='{"Username": "helperuser", "Secret": "pass"}')
|
|
80
|
+
mock.Mock(returncode=0, stdout='{"Username": "helperuser", "Secret": "pass"}'),
|
|
90
81
|
]
|
|
91
|
-
|
|
82
|
+
|
|
92
83
|
with mock.patch("pathlib.Path.home", return_value=tmp_path):
|
|
93
84
|
username = get_docker_username()
|
|
94
|
-
|
|
85
|
+
|
|
95
86
|
assert username == "helperuser"
|
|
96
87
|
|
|
97
88
|
|
|
@@ -101,16 +92,9 @@ class TestGetDockerImageLabels:
|
|
|
101
92
|
@mock.patch("subprocess.run")
|
|
102
93
|
def test_get_labels_success(self, mock_run):
|
|
103
94
|
"""Test successfully getting image labels."""
|
|
104
|
-
labels = {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
mock_run.return_value = mock.Mock(
|
|
109
|
-
returncode=0,
|
|
110
|
-
stdout=json.dumps(labels),
|
|
111
|
-
stderr=""
|
|
112
|
-
)
|
|
113
|
-
|
|
95
|
+
labels = {"org.hud.manifest.head": "abc123", "org.hud.version": "1.0.0"}
|
|
96
|
+
mock_run.return_value = mock.Mock(returncode=0, stdout=json.dumps(labels), stderr="")
|
|
97
|
+
|
|
114
98
|
result = get_docker_image_labels("test:latest")
|
|
115
99
|
assert result == labels
|
|
116
100
|
|
|
@@ -118,7 +102,7 @@ class TestGetDockerImageLabels:
|
|
|
118
102
|
def test_get_labels_failure(self, mock_run):
|
|
119
103
|
"""Test handling failure to get labels."""
|
|
120
104
|
mock_run.side_effect = Exception("Command failed")
|
|
121
|
-
|
|
105
|
+
|
|
122
106
|
result = get_docker_image_labels("test:latest")
|
|
123
107
|
assert result == {}
|
|
124
108
|
|
|
@@ -131,10 +115,10 @@ class TestPushEnvironment:
|
|
|
131
115
|
"""Test pushing when no lock file exists."""
|
|
132
116
|
mock_design = mock.Mock()
|
|
133
117
|
mock_design_class.return_value = mock_design
|
|
134
|
-
|
|
118
|
+
|
|
135
119
|
with pytest.raises(typer.Exit) as exc_info:
|
|
136
120
|
push_environment(str(tmp_path))
|
|
137
|
-
|
|
121
|
+
|
|
138
122
|
assert exc_info.value.exit_code == 1
|
|
139
123
|
mock_design.error.assert_called()
|
|
140
124
|
|
|
@@ -145,14 +129,14 @@ class TestPushEnvironment:
|
|
|
145
129
|
mock_design = mock.Mock()
|
|
146
130
|
mock_design_class.return_value = mock_design
|
|
147
131
|
mock_settings.api_key = None
|
|
148
|
-
|
|
132
|
+
|
|
149
133
|
# Create lock file
|
|
150
134
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
151
135
|
lock_file.write_text(yaml.dump({"image": "test:latest"}))
|
|
152
|
-
|
|
136
|
+
|
|
153
137
|
with pytest.raises(typer.Exit) as exc_info:
|
|
154
138
|
push_environment(str(tmp_path))
|
|
155
|
-
|
|
139
|
+
|
|
156
140
|
assert exc_info.value.exit_code == 1
|
|
157
141
|
|
|
158
142
|
@mock.patch("requests.post")
|
|
@@ -162,8 +146,14 @@ class TestPushEnvironment:
|
|
|
162
146
|
@mock.patch("hud.cli.push.settings")
|
|
163
147
|
@mock.patch("hud.cli.push.HUDDesign")
|
|
164
148
|
def test_push_auto_detect_username(
|
|
165
|
-
self,
|
|
166
|
-
|
|
149
|
+
self,
|
|
150
|
+
mock_design_class,
|
|
151
|
+
mock_settings,
|
|
152
|
+
mock_get_username,
|
|
153
|
+
mock_run,
|
|
154
|
+
mock_popen,
|
|
155
|
+
mock_post,
|
|
156
|
+
tmp_path,
|
|
167
157
|
):
|
|
168
158
|
"""Test auto-detecting Docker username and pushing."""
|
|
169
159
|
# Setup mocks
|
|
@@ -172,15 +162,12 @@ class TestPushEnvironment:
|
|
|
172
162
|
mock_settings.api_key = "test-key"
|
|
173
163
|
mock_settings.hud_telemetry_url = "https://api.hud.test"
|
|
174
164
|
mock_get_username.return_value = "testuser"
|
|
175
|
-
|
|
165
|
+
|
|
176
166
|
# Create lock file
|
|
177
|
-
lock_data = {
|
|
178
|
-
"image": "original/image:v1.0",
|
|
179
|
-
"build": {"version": "0.1.0"}
|
|
180
|
-
}
|
|
167
|
+
lock_data = {"image": "original/image:v1.0", "build": {"version": "0.1.0"}}
|
|
181
168
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
182
169
|
lock_file.write_text(yaml.dump(lock_data))
|
|
183
|
-
|
|
170
|
+
|
|
184
171
|
# Mock docker commands
|
|
185
172
|
def mock_run_impl(*args, **kwargs):
|
|
186
173
|
cmd = args[0]
|
|
@@ -192,50 +179,48 @@ class TestPushEnvironment:
|
|
|
192
179
|
elif cmd[1] == "tag":
|
|
193
180
|
return mock.Mock(returncode=0)
|
|
194
181
|
return mock.Mock(returncode=0)
|
|
195
|
-
|
|
182
|
+
|
|
196
183
|
mock_run.side_effect = mock_run_impl
|
|
197
|
-
|
|
184
|
+
|
|
198
185
|
# Mock docker push
|
|
199
186
|
mock_process = mock.Mock()
|
|
200
187
|
mock_process.stdout = ["Pushing image...", "Push complete"]
|
|
201
188
|
mock_process.wait.return_value = None
|
|
202
189
|
mock_process.returncode = 0
|
|
203
190
|
mock_popen.return_value = mock_process
|
|
204
|
-
|
|
191
|
+
|
|
205
192
|
# Mock registry upload
|
|
206
193
|
mock_post.return_value = mock.Mock(status_code=201)
|
|
207
|
-
|
|
194
|
+
|
|
208
195
|
# Run push
|
|
209
196
|
push_environment(str(tmp_path), yes=True)
|
|
210
|
-
|
|
197
|
+
|
|
211
198
|
# Verify docker commands
|
|
212
199
|
assert mock_run.call_count >= 2
|
|
213
200
|
mock_popen.assert_called_once()
|
|
214
|
-
|
|
201
|
+
|
|
215
202
|
# Verify registry upload
|
|
216
203
|
mock_post.assert_called_once()
|
|
217
204
|
call_args = mock_post.call_args
|
|
218
|
-
assert "testuser/image
|
|
205
|
+
assert "testuser/image%3A0.1.0" in call_args[0][0]
|
|
219
206
|
|
|
220
207
|
@mock.patch("subprocess.run")
|
|
221
208
|
@mock.patch("hud.cli.push.settings")
|
|
222
209
|
@mock.patch("hud.cli.push.HUDDesign")
|
|
223
|
-
def test_push_explicit_image(
|
|
224
|
-
self, mock_design_class, mock_settings, mock_run, tmp_path
|
|
225
|
-
):
|
|
210
|
+
def test_push_explicit_image(self, mock_design_class, mock_settings, mock_run, tmp_path):
|
|
226
211
|
"""Test pushing with explicit image name."""
|
|
227
212
|
mock_design = mock.Mock()
|
|
228
213
|
mock_design_class.return_value = mock_design
|
|
229
214
|
mock_settings.api_key = "test-key"
|
|
230
|
-
|
|
215
|
+
|
|
231
216
|
# Create lock file
|
|
232
217
|
lock_data = {"image": "local:latest"}
|
|
233
218
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
234
219
|
lock_file.write_text(yaml.dump(lock_data))
|
|
235
|
-
|
|
220
|
+
|
|
236
221
|
# Mock docker inspect for non-existent local image
|
|
237
222
|
mock_run.side_effect = subprocess.CalledProcessError(1, "docker")
|
|
238
|
-
|
|
223
|
+
|
|
239
224
|
with pytest.raises(typer.Exit):
|
|
240
225
|
push_environment(str(tmp_path), image="myrepo/myimage:v2")
|
|
241
226
|
|
|
@@ -243,19 +228,17 @@ class TestPushEnvironment:
|
|
|
243
228
|
@mock.patch("subprocess.run")
|
|
244
229
|
@mock.patch("hud.cli.push.settings")
|
|
245
230
|
@mock.patch("hud.cli.push.HUDDesign")
|
|
246
|
-
def test_push_with_tag(
|
|
247
|
-
self, mock_design_class, mock_settings, mock_run, mock_popen, tmp_path
|
|
248
|
-
):
|
|
231
|
+
def test_push_with_tag(self, mock_design_class, mock_settings, mock_run, mock_popen, tmp_path):
|
|
249
232
|
"""Test pushing with explicit tag."""
|
|
250
233
|
mock_design = mock.Mock()
|
|
251
234
|
mock_design_class.return_value = mock_design
|
|
252
235
|
mock_settings.api_key = "test-key"
|
|
253
|
-
|
|
236
|
+
|
|
254
237
|
# Create lock file
|
|
255
238
|
lock_data = {"image": "test:latest"}
|
|
256
239
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
257
240
|
lock_file.write_text(yaml.dump(lock_data))
|
|
258
|
-
|
|
241
|
+
|
|
259
242
|
# Mock docker commands
|
|
260
243
|
def mock_run_impl(*args, **kwargs):
|
|
261
244
|
cmd = args[0]
|
|
@@ -267,19 +250,19 @@ class TestPushEnvironment:
|
|
|
267
250
|
elif cmd[1] == "tag":
|
|
268
251
|
return mock.Mock(returncode=0)
|
|
269
252
|
return mock.Mock(returncode=0)
|
|
270
|
-
|
|
253
|
+
|
|
271
254
|
mock_run.side_effect = mock_run_impl
|
|
272
|
-
|
|
255
|
+
|
|
273
256
|
# Mock docker push
|
|
274
257
|
mock_process = mock.Mock()
|
|
275
258
|
mock_process.stdout = []
|
|
276
259
|
mock_process.wait.return_value = None
|
|
277
260
|
mock_process.returncode = 0
|
|
278
261
|
mock_popen.return_value = mock_process
|
|
279
|
-
|
|
262
|
+
|
|
280
263
|
# Run push
|
|
281
264
|
push_environment(str(tmp_path), image="user/test", tag="v2.0", yes=True)
|
|
282
|
-
|
|
265
|
+
|
|
283
266
|
# Verify tag was used
|
|
284
267
|
tag_call = [c for c in mock_run.call_args_list if c[0][0][1] == "tag"]
|
|
285
268
|
assert len(tag_call) > 0
|
|
@@ -291,19 +274,21 @@ class TestPushEnvironment:
|
|
|
291
274
|
"""Test handling Docker push failure."""
|
|
292
275
|
mock_design = mock.Mock()
|
|
293
276
|
mock_design_class.return_value = mock_design
|
|
294
|
-
|
|
277
|
+
|
|
295
278
|
# Mock docker push failure
|
|
296
279
|
mock_process = mock.Mock()
|
|
297
280
|
mock_process.stdout = ["Error: access denied"]
|
|
298
281
|
mock_process.wait.return_value = None
|
|
299
282
|
mock_process.returncode = 1
|
|
300
283
|
mock_popen.return_value = mock_process
|
|
301
|
-
|
|
284
|
+
|
|
302
285
|
with mock.patch("hud.cli.push.settings") as mock_settings:
|
|
303
286
|
mock_settings.api_key = "test-key"
|
|
304
|
-
with
|
|
305
|
-
|
|
306
|
-
|
|
287
|
+
with (
|
|
288
|
+
mock.patch("subprocess.run"),
|
|
289
|
+
pytest.raises(typer.Exit),
|
|
290
|
+
):
|
|
291
|
+
push_environment(".", image="test:latest", yes=True)
|
|
307
292
|
|
|
308
293
|
@mock.patch("hud.cli.push.get_docker_image_labels")
|
|
309
294
|
@mock.patch("subprocess.run")
|
|
@@ -316,18 +301,18 @@ class TestPushEnvironment:
|
|
|
316
301
|
mock_design = mock.Mock()
|
|
317
302
|
mock_design_class.return_value = mock_design
|
|
318
303
|
mock_settings.api_key = "test-key"
|
|
319
|
-
|
|
304
|
+
|
|
320
305
|
# Create lock file
|
|
321
306
|
lock_data = {"image": "test:latest"}
|
|
322
307
|
lock_file = tmp_path / "hud.lock.yaml"
|
|
323
308
|
lock_file.write_text(yaml.dump(lock_data))
|
|
324
|
-
|
|
309
|
+
|
|
325
310
|
# Mock labels
|
|
326
311
|
mock_get_labels.return_value = {
|
|
327
312
|
"org.hud.manifest.head": "abc123def456",
|
|
328
|
-
"org.hud.version": "1.2.3"
|
|
313
|
+
"org.hud.version": "1.2.3",
|
|
329
314
|
}
|
|
330
|
-
|
|
315
|
+
|
|
331
316
|
# Mock docker commands - first inspect succeeds to get to label check
|
|
332
317
|
# Provide explicit image to bypass username check
|
|
333
318
|
def mock_run_impl(*args, **kwargs):
|
|
@@ -339,13 +324,13 @@ class TestPushEnvironment:
|
|
|
339
324
|
# Fail on tag to exit after labels are checked
|
|
340
325
|
raise subprocess.CalledProcessError(1, cmd)
|
|
341
326
|
return mock.Mock(returncode=0)
|
|
342
|
-
|
|
327
|
+
|
|
343
328
|
mock_run.side_effect = mock_run_impl
|
|
344
|
-
|
|
329
|
+
|
|
345
330
|
# Provide explicit image to ensure we reach label check
|
|
346
331
|
with pytest.raises(subprocess.CalledProcessError):
|
|
347
332
|
push_environment(str(tmp_path), image="test:v2", verbose=True)
|
|
348
|
-
|
|
333
|
+
|
|
349
334
|
# Verify labels were checked
|
|
350
335
|
mock_get_labels.assert_called_once_with("test:latest")
|
|
351
336
|
|
|
@@ -357,10 +342,8 @@ class TestPushCommand:
|
|
|
357
342
|
"""Test basic push command."""
|
|
358
343
|
with mock.patch("hud.cli.push.push_environment") as mock_push:
|
|
359
344
|
push_command()
|
|
360
|
-
|
|
361
|
-
mock_push.assert_called_once_with(
|
|
362
|
-
".", None, None, False, False, False
|
|
363
|
-
)
|
|
345
|
+
|
|
346
|
+
mock_push.assert_called_once_with(".", None, None, False, False, False)
|
|
364
347
|
|
|
365
348
|
def test_push_command_with_options(self):
|
|
366
349
|
"""Test push command with all options."""
|
|
@@ -371,9 +354,7 @@ class TestPushCommand:
|
|
|
371
354
|
tag="v1.0",
|
|
372
355
|
sign=True,
|
|
373
356
|
yes=True,
|
|
374
|
-
verbose=True
|
|
375
|
-
)
|
|
376
|
-
|
|
377
|
-
mock_push.assert_called_once_with(
|
|
378
|
-
"./myenv", "myrepo/myimage", "v1.0", True, True, True
|
|
357
|
+
verbose=True,
|
|
379
358
|
)
|
|
359
|
+
|
|
360
|
+
mock_push.assert_called_once_with("./myenv", "myrepo/myimage", "v1.0", True, True, True)
|