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/push.py
CHANGED
|
@@ -21,6 +21,7 @@ def _get_response_text(response: requests.Response) -> str:
|
|
|
21
21
|
except Exception:
|
|
22
22
|
return response.text
|
|
23
23
|
|
|
24
|
+
|
|
24
25
|
def get_docker_username() -> str | None:
|
|
25
26
|
"""Get the current Docker username if logged in."""
|
|
26
27
|
try:
|
|
@@ -53,10 +54,10 @@ def get_docker_username() -> str | None:
|
|
|
53
54
|
username = decoded.split(":", 1)[0]
|
|
54
55
|
if username and username != "token": # Skip token-based auth
|
|
55
56
|
return username
|
|
56
|
-
except Exception:
|
|
57
|
-
pass
|
|
58
|
-
except Exception:
|
|
59
|
-
pass
|
|
57
|
+
except Exception: # noqa: S110
|
|
58
|
+
pass
|
|
59
|
+
except Exception: # noqa: S110
|
|
60
|
+
pass
|
|
60
61
|
|
|
61
62
|
# Alternative: Check credsStore/credHelpers
|
|
62
63
|
for config_path in config_paths:
|
|
@@ -91,12 +92,12 @@ def get_docker_username() -> str | None:
|
|
|
91
92
|
username = cred_data.get("Username", "")
|
|
92
93
|
if username and username != "token":
|
|
93
94
|
return username
|
|
94
|
-
except Exception:
|
|
95
|
-
pass
|
|
96
|
-
except Exception:
|
|
97
|
-
pass
|
|
98
|
-
except Exception:
|
|
99
|
-
pass
|
|
95
|
+
except Exception: # noqa: S110
|
|
96
|
+
pass
|
|
97
|
+
except Exception: # noqa: S110
|
|
98
|
+
pass
|
|
99
|
+
except Exception: # noqa: S110
|
|
100
|
+
pass
|
|
100
101
|
return None
|
|
101
102
|
|
|
102
103
|
|
|
@@ -155,7 +156,7 @@ def push_environment(
|
|
|
155
156
|
if not local_image and "build" in lock_data:
|
|
156
157
|
# New format might have image elsewhere
|
|
157
158
|
local_image = lock_data.get("image", "")
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
# Get internal version from lock file
|
|
160
161
|
internal_version = lock_data.get("build", {}).get("version", None)
|
|
161
162
|
|
|
@@ -206,21 +207,25 @@ def push_environment(
|
|
|
206
207
|
# Handle tag when image is provided
|
|
207
208
|
# Prefer explicit tag over internal version
|
|
208
209
|
final_tag = tag if tag else internal_version
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
if ":" in image:
|
|
211
212
|
# Image already has a tag
|
|
212
213
|
existing_tag = image.split(":")[-1]
|
|
213
214
|
if existing_tag != final_tag:
|
|
214
215
|
if tag:
|
|
215
|
-
design.warning(
|
|
216
|
+
design.warning(
|
|
217
|
+
f"Image already has tag '{existing_tag}', overriding with '{final_tag}'"
|
|
218
|
+
)
|
|
216
219
|
else:
|
|
217
|
-
design.info(
|
|
220
|
+
design.info(
|
|
221
|
+
f"Image has tag '{existing_tag}', but using internal version '{final_tag}'"
|
|
222
|
+
)
|
|
218
223
|
image = image.rsplit(":", 1)[0] + f":{final_tag}"
|
|
219
224
|
# else: tags match, no action needed
|
|
220
225
|
else:
|
|
221
226
|
# Image has no tag, append the appropriate one
|
|
222
227
|
image = f"{image}:{final_tag}"
|
|
223
|
-
|
|
228
|
+
|
|
224
229
|
if tag:
|
|
225
230
|
design.info(f"Using specified tag: {tag}")
|
|
226
231
|
else:
|
|
@@ -230,7 +235,7 @@ def push_environment(
|
|
|
230
235
|
# Verify local image exists
|
|
231
236
|
# Extract the tag part (before @sha256:...) for Docker operations
|
|
232
237
|
local_tag = local_image.split("@")[0] if "@" in local_image else local_image
|
|
233
|
-
|
|
238
|
+
|
|
234
239
|
# Also check for version-tagged image if we have internal version
|
|
235
240
|
version_tag = None
|
|
236
241
|
if internal_version and ":" in local_tag:
|
|
@@ -246,7 +251,7 @@ def push_environment(
|
|
|
246
251
|
design.info(f"Found version-tagged image: {version_tag}")
|
|
247
252
|
except subprocess.CalledProcessError:
|
|
248
253
|
pass
|
|
249
|
-
|
|
254
|
+
|
|
250
255
|
if not image_to_push:
|
|
251
256
|
try:
|
|
252
257
|
subprocess.run(["docker", "inspect", local_tag], capture_output=True, check=True) # noqa: S603, S607
|
|
@@ -319,7 +324,7 @@ def push_environment(
|
|
|
319
324
|
lock_data["image"] = pushed_digest
|
|
320
325
|
|
|
321
326
|
# Add push information
|
|
322
|
-
from datetime import
|
|
327
|
+
from datetime import UTC, datetime
|
|
323
328
|
|
|
324
329
|
lock_data["push"] = {
|
|
325
330
|
"source": local_image,
|
|
@@ -340,13 +345,20 @@ def push_environment(
|
|
|
340
345
|
# e.g., "hudpython/test_init:v1.0" -> "hudpython/test_init:v1.0"
|
|
341
346
|
# Use the original image name for the registry path, not the digest
|
|
342
347
|
# The digest might not contain the tag information
|
|
343
|
-
registry_image =
|
|
344
|
-
|
|
348
|
+
registry_image = (
|
|
349
|
+
image # This is the image we tagged and pushed (e.g., hudpython/hud-text-2048:0.1.2)
|
|
350
|
+
)
|
|
351
|
+
|
|
345
352
|
# Remove any registry prefix for the HUD registry path
|
|
346
353
|
registry_parts = registry_image.split("/")
|
|
347
354
|
if len(registry_parts) >= 2:
|
|
348
355
|
# Handle docker.io/org/name or just org/name
|
|
349
|
-
if registry_parts[0] in [
|
|
356
|
+
if registry_parts[0] in [
|
|
357
|
+
"docker.io",
|
|
358
|
+
"registry-1.docker.io",
|
|
359
|
+
"index.docker.io",
|
|
360
|
+
"ghcr.io",
|
|
361
|
+
]:
|
|
350
362
|
# Remove registry prefix
|
|
351
363
|
name_with_tag = "/".join(registry_parts[1:])
|
|
352
364
|
elif "." in registry_parts[0] or ":" in registry_parts[0]:
|
|
@@ -359,12 +371,12 @@ def push_environment(
|
|
|
359
371
|
name_with_tag = registry_image
|
|
360
372
|
|
|
361
373
|
# The image variable already has the tag, no need to add :latest
|
|
362
|
-
|
|
374
|
+
|
|
363
375
|
# Validate the image format
|
|
364
376
|
if not name_with_tag:
|
|
365
377
|
design.warning("Could not determine image name for registry upload")
|
|
366
378
|
raise typer.Exit(0)
|
|
367
|
-
|
|
379
|
+
|
|
368
380
|
# For HUD registry, we need org/name format
|
|
369
381
|
if "/" not in name_with_tag:
|
|
370
382
|
design.warning("Image name must include organization/namespace for HUD registry")
|
hud/cli/remove.py
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import shutil
|
|
6
|
-
from pathlib import Path
|
|
7
6
|
|
|
8
7
|
import typer
|
|
9
|
-
import yaml
|
|
10
8
|
|
|
11
9
|
from hud.utils.design import HUDDesign
|
|
12
10
|
|
|
13
|
-
from .registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
11
|
+
from .utils.registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
def remove_environment(
|
|
@@ -21,18 +19,18 @@ def remove_environment(
|
|
|
21
19
|
"""Remove an environment from the local registry."""
|
|
22
20
|
design = HUDDesign()
|
|
23
21
|
design.header("HUD Environment Removal")
|
|
24
|
-
|
|
22
|
+
|
|
25
23
|
# Find the environment to remove
|
|
26
24
|
found_entry = None
|
|
27
25
|
found_digest = None
|
|
28
|
-
|
|
26
|
+
|
|
29
27
|
# First check if target is a digest
|
|
30
28
|
for digest, lock_file in list_registry_entries():
|
|
31
29
|
if digest.startswith(target):
|
|
32
30
|
found_entry = lock_file
|
|
33
31
|
found_digest = digest
|
|
34
32
|
break
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
# If not found by digest, search by name
|
|
37
35
|
if not found_entry:
|
|
38
36
|
for digest, lock_file in list_registry_entries():
|
|
@@ -42,28 +40,29 @@ def remove_environment(
|
|
|
42
40
|
image = lock_data["image"]
|
|
43
41
|
# Extract name and tag
|
|
44
42
|
name = image.split("@")[0] if "@" in image else image
|
|
45
|
-
if "/" in name:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
except Exception:
|
|
43
|
+
if "/" in name and (target in name or name.endswith(f"/{target}")):
|
|
44
|
+
found_entry = lock_file
|
|
45
|
+
found_digest = digest
|
|
46
|
+
break
|
|
47
|
+
except Exception as e:
|
|
48
|
+
design.error(f"Error loading lock file: {e}")
|
|
52
49
|
continue
|
|
53
|
-
|
|
50
|
+
|
|
54
51
|
if not found_entry:
|
|
55
52
|
design.error(f"Environment not found: {target}")
|
|
56
53
|
design.info("Use 'hud list' to see available environments")
|
|
57
54
|
raise typer.Exit(1)
|
|
58
|
-
|
|
55
|
+
|
|
59
56
|
# Load and display environment info
|
|
60
57
|
try:
|
|
58
|
+
if found_digest is None:
|
|
59
|
+
raise ValueError("Found digest is None")
|
|
61
60
|
lock_data = load_from_registry(found_digest)
|
|
62
61
|
if lock_data:
|
|
63
62
|
image = lock_data.get("image", "unknown")
|
|
64
63
|
metadata = lock_data.get("metadata", {})
|
|
65
64
|
description = metadata.get("description", "No description")
|
|
66
|
-
|
|
65
|
+
|
|
67
66
|
design.section_title("Environment Details")
|
|
68
67
|
design.status_item("Image", image)
|
|
69
68
|
design.status_item("Digest", found_digest)
|
|
@@ -72,20 +71,20 @@ def remove_environment(
|
|
|
72
71
|
except Exception as e:
|
|
73
72
|
if verbose:
|
|
74
73
|
design.warning(f"Could not read environment details: {e}")
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
# Confirm deletion
|
|
77
76
|
if not yes:
|
|
78
77
|
design.info("")
|
|
79
78
|
if not typer.confirm(f"Remove environment {found_digest}?"):
|
|
80
79
|
design.info("Aborted")
|
|
81
80
|
raise typer.Exit(0)
|
|
82
|
-
|
|
81
|
+
|
|
83
82
|
# Remove the environment directory
|
|
84
83
|
try:
|
|
85
84
|
env_dir = found_entry.parent
|
|
86
85
|
shutil.rmtree(env_dir)
|
|
87
86
|
design.success(f"Removed environment: {found_digest}")
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
# Check if the image is still available locally
|
|
90
89
|
if lock_data:
|
|
91
90
|
image = lock_data.get("image", "")
|
|
@@ -95,7 +94,7 @@ def remove_environment(
|
|
|
95
94
|
design.info(f"To remove it, run: [cyan]docker rmi {image.split('@')[0]}[/cyan]")
|
|
96
95
|
except Exception as e:
|
|
97
96
|
design.error(f"Failed to remove environment: {e}")
|
|
98
|
-
raise typer.Exit(1)
|
|
97
|
+
raise typer.Exit(1) from e
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
def remove_all_environments(
|
|
@@ -105,23 +104,23 @@ def remove_all_environments(
|
|
|
105
104
|
"""Remove all environments from the local registry."""
|
|
106
105
|
design = HUDDesign()
|
|
107
106
|
design.header("Remove All HUD Environments")
|
|
108
|
-
|
|
107
|
+
|
|
109
108
|
registry_dir = get_registry_dir()
|
|
110
109
|
if not registry_dir.exists():
|
|
111
110
|
design.info("No environments found in local registry.")
|
|
112
111
|
return
|
|
113
|
-
|
|
112
|
+
|
|
114
113
|
# Count environments
|
|
115
114
|
entries = list(list_registry_entries())
|
|
116
115
|
if not entries:
|
|
117
116
|
design.info("No environments found in local registry.")
|
|
118
117
|
return
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
design.warning(f"This will remove {len(entries)} environment(s) from the local registry!")
|
|
121
|
-
|
|
120
|
+
|
|
122
121
|
# List environments that will be removed
|
|
123
122
|
design.section_title("Environments to Remove")
|
|
124
|
-
for digest,
|
|
123
|
+
for digest, _ in entries:
|
|
125
124
|
try:
|
|
126
125
|
lock_data = load_from_registry(digest)
|
|
127
126
|
if lock_data:
|
|
@@ -129,18 +128,18 @@ def remove_all_environments(
|
|
|
129
128
|
design.info(f" • {digest[:12]} - {image}")
|
|
130
129
|
except Exception:
|
|
131
130
|
design.info(f" • {digest[:12]}")
|
|
132
|
-
|
|
131
|
+
|
|
133
132
|
# Confirm deletion
|
|
134
133
|
if not yes:
|
|
135
134
|
design.info("")
|
|
136
135
|
if not typer.confirm("Remove ALL environments?", default=False):
|
|
137
136
|
design.info("Aborted")
|
|
138
137
|
raise typer.Exit(0)
|
|
139
|
-
|
|
138
|
+
|
|
140
139
|
# Remove all environments
|
|
141
140
|
removed = 0
|
|
142
141
|
failed = 0
|
|
143
|
-
|
|
142
|
+
|
|
144
143
|
for digest, lock_file in entries:
|
|
145
144
|
try:
|
|
146
145
|
env_dir = lock_file.parent
|
|
@@ -152,13 +151,13 @@ def remove_all_environments(
|
|
|
152
151
|
failed += 1
|
|
153
152
|
if verbose:
|
|
154
153
|
design.error(f"Failed to remove {digest}: {e}")
|
|
155
|
-
|
|
154
|
+
|
|
156
155
|
design.info("")
|
|
157
156
|
if failed == 0:
|
|
158
157
|
design.success(f"Successfully removed {removed} environment(s)")
|
|
159
158
|
else:
|
|
160
159
|
design.warning(f"Removed {removed} environment(s), failed to remove {failed}")
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
design.info("")
|
|
163
162
|
design.info("Note: Docker images may still exist locally.")
|
|
164
163
|
design.info("To remove them, use: [cyan]docker image prune[/cyan]")
|
|
@@ -166,21 +165,16 @@ def remove_all_environments(
|
|
|
166
165
|
|
|
167
166
|
def remove_command(
|
|
168
167
|
target: str | None = typer.Argument(
|
|
169
|
-
None,
|
|
170
|
-
help="Environment to remove (digest, name, or 'all' for all environments)"
|
|
171
|
-
),
|
|
172
|
-
yes: bool = typer.Option(
|
|
173
|
-
False, "--yes", "-y", help="Skip confirmation prompt"
|
|
174
|
-
),
|
|
175
|
-
verbose: bool = typer.Option(
|
|
176
|
-
False, "--verbose", "-v", help="Show detailed output"
|
|
168
|
+
None, help="Environment to remove (digest, name, or 'all' for all environments)"
|
|
177
169
|
),
|
|
170
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
171
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output"),
|
|
178
172
|
) -> None:
|
|
179
173
|
"""🗑️ Remove HUD environments from local registry.
|
|
180
|
-
|
|
174
|
+
|
|
181
175
|
Removes environment metadata from ~/.hud/envs/
|
|
182
176
|
Note: This does not remove the Docker images.
|
|
183
|
-
|
|
177
|
+
|
|
184
178
|
Examples:
|
|
185
179
|
hud remove abc123 # Remove by digest
|
|
186
180
|
hud remove text_2048 # Remove by name
|
|
@@ -193,7 +187,7 @@ def remove_command(
|
|
|
193
187
|
design.error("Please specify an environment to remove or 'all'")
|
|
194
188
|
design.info("Use 'hud list' to see available environments")
|
|
195
189
|
raise typer.Exit(1)
|
|
196
|
-
|
|
190
|
+
|
|
197
191
|
if target.lower() == "all":
|
|
198
192
|
remove_all_environments(yes, verbose)
|
|
199
193
|
else:
|
hud/cli/tests/test_analyze.py
CHANGED
|
@@ -82,6 +82,7 @@ class TestAnalyzeEnvironment:
|
|
|
82
82
|
with (
|
|
83
83
|
patch("hud.cli.analyze.MCPClient") as MockClient,
|
|
84
84
|
patch("hud.cli.analyze.console") as mock_console,
|
|
85
|
+
patch("platform.system", return_value="Windows"),
|
|
85
86
|
):
|
|
86
87
|
# Setup mock client that will raise exception during initialization
|
|
87
88
|
mock_client = MagicMock()
|
|
@@ -100,7 +101,7 @@ class TestAnalyzeEnvironment:
|
|
|
100
101
|
mock_client.initialize.assert_called_once()
|
|
101
102
|
mock_client.shutdown.assert_called_once()
|
|
102
103
|
|
|
103
|
-
# Check console printed error hints
|
|
104
|
+
# Check console printed Windows-specific error hints
|
|
104
105
|
calls = mock_console.print.call_args_list
|
|
105
106
|
assert any("Docker logs may not show on Windows" in str(call) for call in calls)
|
|
106
107
|
|
|
@@ -3,20 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import tempfile
|
|
8
|
-
from pathlib import Path
|
|
9
6
|
from unittest import mock
|
|
10
7
|
|
|
11
8
|
import pytest
|
|
12
9
|
import yaml
|
|
13
10
|
|
|
14
|
-
from hud.cli.
|
|
11
|
+
from hud.cli.utils.metadata import (
|
|
15
12
|
analyze_from_metadata,
|
|
16
13
|
check_local_cache,
|
|
17
14
|
fetch_lock_from_registry,
|
|
18
15
|
)
|
|
19
|
-
from hud.cli.registry import save_to_registry
|
|
16
|
+
from hud.cli.utils.registry import save_to_registry
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
@pytest.fixture
|
|
@@ -79,9 +76,7 @@ class TestFetchLockFromRegistry:
|
|
|
79
76
|
"""Test successful fetch from registry."""
|
|
80
77
|
mock_response = mock.Mock()
|
|
81
78
|
mock_response.status_code = 200
|
|
82
|
-
mock_response.json.return_value = {
|
|
83
|
-
"lock": yaml.dump({"test": "data"})
|
|
84
|
-
}
|
|
79
|
+
mock_response.json.return_value = {"lock": yaml.dump({"test": "data"})}
|
|
85
80
|
mock_get.return_value = mock_response
|
|
86
81
|
|
|
87
82
|
result = fetch_lock_from_registry("test/env:latest")
|
|
@@ -93,9 +88,7 @@ class TestFetchLockFromRegistry:
|
|
|
93
88
|
"""Test fetch when response has lock_data key."""
|
|
94
89
|
mock_response = mock.Mock()
|
|
95
90
|
mock_response.status_code = 200
|
|
96
|
-
mock_response.json.return_value = {
|
|
97
|
-
"lock_data": {"test": "data"}
|
|
98
|
-
}
|
|
91
|
+
mock_response.json.return_value = {"lock_data": {"test": "data"}}
|
|
99
92
|
mock_get.return_value = mock_response
|
|
100
93
|
|
|
101
94
|
result = fetch_lock_from_registry("test/env:latest")
|
|
@@ -120,10 +113,10 @@ class TestFetchLockFromRegistry:
|
|
|
120
113
|
mock_get.return_value = mock_response
|
|
121
114
|
|
|
122
115
|
fetch_lock_from_registry("test/env")
|
|
123
|
-
|
|
124
|
-
# Check that the URL includes :latest
|
|
116
|
+
|
|
117
|
+
# Check that the URL includes :latest (URL-encoded)
|
|
125
118
|
call_args = mock_get.call_args
|
|
126
|
-
assert "test/env
|
|
119
|
+
assert "test/env%3Alatest" in call_args[0][0]
|
|
127
120
|
|
|
128
121
|
@mock.patch("requests.get")
|
|
129
122
|
def test_fetch_lock_failure(self, mock_get):
|
|
@@ -150,11 +143,11 @@ class TestCheckLocalCache:
|
|
|
150
143
|
def test_check_local_cache_found(self, mock_registry_dir, sample_lock_data, monkeypatch):
|
|
151
144
|
"""Test finding lock data in local cache."""
|
|
152
145
|
# Mock registry directory
|
|
153
|
-
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
154
|
-
|
|
146
|
+
monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
147
|
+
|
|
155
148
|
# Save sample data to registry
|
|
156
149
|
save_to_registry(sample_lock_data, "test/environment:latest", verbose=False)
|
|
157
|
-
|
|
150
|
+
|
|
158
151
|
# Check cache
|
|
159
152
|
result = check_local_cache("test/environment:latest")
|
|
160
153
|
assert result is not None
|
|
@@ -162,21 +155,21 @@ class TestCheckLocalCache:
|
|
|
162
155
|
|
|
163
156
|
def test_check_local_cache_not_found(self, mock_registry_dir, monkeypatch):
|
|
164
157
|
"""Test when lock data not in local cache."""
|
|
165
|
-
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
166
|
-
|
|
158
|
+
monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
159
|
+
|
|
167
160
|
result = check_local_cache("nonexistent/env:latest")
|
|
168
161
|
assert result is None
|
|
169
162
|
|
|
170
163
|
def test_check_local_cache_invalid_yaml(self, mock_registry_dir, monkeypatch):
|
|
171
164
|
"""Test when lock file has invalid YAML."""
|
|
172
|
-
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
173
|
-
|
|
165
|
+
monkeypatch.setattr("hud.cli.utils.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
166
|
+
|
|
174
167
|
# Create invalid lock file
|
|
175
|
-
digest = "
|
|
168
|
+
digest = "invalid"
|
|
176
169
|
lock_file = mock_registry_dir / digest / "hud.lock.yaml"
|
|
177
170
|
lock_file.parent.mkdir(parents=True)
|
|
178
171
|
lock_file.write_text("invalid: yaml: content:")
|
|
179
|
-
|
|
172
|
+
|
|
180
173
|
result = check_local_cache("test/invalid:latest")
|
|
181
174
|
assert result is None
|
|
182
175
|
|
|
@@ -189,60 +182,60 @@ class TestCheckLocalCache:
|
|
|
189
182
|
class TestAnalyzeFromMetadata:
|
|
190
183
|
"""Test the main analyze_from_metadata function."""
|
|
191
184
|
|
|
192
|
-
@mock.patch("hud.cli.
|
|
193
|
-
@mock.patch("hud.cli.
|
|
185
|
+
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
186
|
+
@mock.patch("hud.cli.utils.metadata.console")
|
|
194
187
|
async def test_analyze_from_local_cache(self, mock_console, mock_check, sample_lock_data):
|
|
195
188
|
"""Test analyzing from local cache."""
|
|
196
189
|
mock_check.return_value = sample_lock_data
|
|
197
|
-
|
|
190
|
+
|
|
198
191
|
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
199
|
-
|
|
192
|
+
|
|
200
193
|
mock_check.assert_called_once_with("test/env:latest")
|
|
201
194
|
# Should output JSON
|
|
202
195
|
mock_console.print_json.assert_called_once()
|
|
203
196
|
|
|
204
|
-
@mock.patch("hud.cli.
|
|
205
|
-
@mock.patch("hud.cli.
|
|
206
|
-
@mock.patch("hud.cli.
|
|
207
|
-
@mock.patch("hud.cli.
|
|
197
|
+
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
198
|
+
@mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
|
|
199
|
+
@mock.patch("hud.cli.utils.registry.save_to_registry")
|
|
200
|
+
@mock.patch("hud.cli.utils.metadata.console")
|
|
208
201
|
async def test_analyze_from_registry(
|
|
209
202
|
self, mock_console, mock_save, mock_fetch, mock_check, sample_lock_data
|
|
210
203
|
):
|
|
211
204
|
"""Test analyzing from registry when not in cache."""
|
|
212
205
|
mock_check.return_value = None
|
|
213
206
|
mock_fetch.return_value = sample_lock_data
|
|
214
|
-
|
|
207
|
+
|
|
215
208
|
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
216
|
-
|
|
209
|
+
|
|
217
210
|
mock_check.assert_called_once()
|
|
218
211
|
mock_fetch.assert_called_once()
|
|
219
212
|
mock_save.assert_called_once() # Should save to cache
|
|
220
213
|
mock_console.print_json.assert_called_once()
|
|
221
214
|
|
|
222
|
-
@mock.patch("hud.cli.
|
|
223
|
-
@mock.patch("hud.cli.
|
|
224
|
-
@mock.patch("hud.cli.
|
|
225
|
-
@mock.patch("hud.cli.
|
|
215
|
+
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
216
|
+
@mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
|
|
217
|
+
@mock.patch("hud.cli.utils.metadata.design")
|
|
218
|
+
@mock.patch("hud.cli.utils.metadata.console")
|
|
226
219
|
async def test_analyze_not_found(self, mock_console, mock_design, mock_fetch, mock_check):
|
|
227
220
|
"""Test when environment not found anywhere."""
|
|
228
221
|
mock_check.return_value = None
|
|
229
222
|
mock_fetch.return_value = None
|
|
230
|
-
|
|
223
|
+
|
|
231
224
|
await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
|
|
232
|
-
|
|
225
|
+
|
|
233
226
|
# Should show error
|
|
234
227
|
mock_design.error.assert_called_with("Environment metadata not found")
|
|
235
228
|
# Should print suggestions
|
|
236
229
|
mock_console.print.assert_called()
|
|
237
230
|
|
|
238
|
-
@mock.patch("hud.cli.
|
|
239
|
-
@mock.patch("hud.cli.
|
|
231
|
+
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
232
|
+
@mock.patch("hud.cli.utils.metadata.console")
|
|
240
233
|
async def test_analyze_verbose_mode(self, mock_console, mock_check, sample_lock_data):
|
|
241
234
|
"""Test verbose mode includes input schemas."""
|
|
242
235
|
mock_check.return_value = sample_lock_data
|
|
243
|
-
|
|
236
|
+
|
|
244
237
|
await analyze_from_metadata("test/env:latest", "json", verbose=True)
|
|
245
|
-
|
|
238
|
+
|
|
246
239
|
# In verbose mode, the JSON output should include input schemas
|
|
247
240
|
mock_console.print_json.assert_called_once()
|
|
248
241
|
# Get the JSON string that was printed
|
|
@@ -250,13 +243,13 @@ class TestAnalyzeFromMetadata:
|
|
|
250
243
|
output_data = json.loads(call_args)
|
|
251
244
|
assert "inputSchema" in output_data["tools"][0]
|
|
252
245
|
|
|
253
|
-
@mock.patch("hud.cli.
|
|
254
|
-
@mock.patch("hud.cli.
|
|
246
|
+
@mock.patch("hud.cli.utils.metadata.check_local_cache")
|
|
247
|
+
@mock.patch("hud.cli.utils.metadata.fetch_lock_from_registry")
|
|
255
248
|
async def test_analyze_registry_reference_parsing(self, mock_fetch, mock_check):
|
|
256
249
|
"""Test parsing of different registry reference formats."""
|
|
257
250
|
mock_check.return_value = None
|
|
258
251
|
mock_fetch.return_value = {"test": "data"}
|
|
259
|
-
|
|
252
|
+
|
|
260
253
|
# Test different reference formats
|
|
261
254
|
test_cases = [
|
|
262
255
|
("docker.io/org/name:tag", "org/name:tag"),
|
|
@@ -265,13 +258,13 @@ class TestAnalyzeFromMetadata:
|
|
|
265
258
|
("org/name", "org/name"),
|
|
266
259
|
("name:tag", "name:tag"),
|
|
267
260
|
]
|
|
268
|
-
|
|
261
|
+
|
|
269
262
|
for input_ref, expected_call in test_cases:
|
|
270
263
|
await analyze_from_metadata(input_ref, "json", verbose=False)
|
|
271
|
-
|
|
264
|
+
|
|
272
265
|
# Check what was passed to fetch_lock_from_registry
|
|
273
266
|
calls = mock_fetch.call_args_list
|
|
274
267
|
last_call = calls[-1][0][0]
|
|
275
|
-
|
|
268
|
+
|
|
276
269
|
# The function might add :latest, so check base name
|
|
277
270
|
assert expected_call.split(":")[0] in last_call
|