hud-python 0.4.7__py3-none-any.whl → 0.4.9__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/agents/base.py +50 -1
- hud/cli/__init__.py +120 -1
- hud/cli/analyze_metadata.py +29 -41
- hud/cli/build.py +7 -0
- hud/cli/debug.py +8 -1
- hud/cli/eval.py +226 -0
- hud/cli/list_func.py +212 -0
- hud/cli/pull.py +4 -13
- hud/cli/push.py +84 -41
- hud/cli/registry.py +155 -0
- hud/cli/remove.py +200 -0
- hud/cli/tests/test_analyze_metadata.py +277 -0
- hud/cli/tests/test_build.py +450 -0
- hud/cli/tests/test_list_func.py +288 -0
- hud/cli/tests/test_pull.py +400 -0
- hud/cli/tests/test_push.py +379 -0
- hud/cli/tests/test_registry.py +264 -0
- hud/clients/base.py +13 -1
- hud/clients/fastmcp.py +13 -9
- hud/clients/mcp_use.py +1 -1
- hud/tools/__init__.py +2 -0
- hud/tools/response.py +54 -0
- hud/utils/design.py +10 -0
- hud/utils/mcp.py +14 -2
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.7.dist-info → hud_python-0.4.9.dist-info}/METADATA +13 -1
- {hud_python-0.4.7.dist-info → hud_python-0.4.9.dist-info}/RECORD +31 -20
- {hud_python-0.4.7.dist-info → hud_python-0.4.9.dist-info}/WHEEL +0 -0
- {hud_python-0.4.7.dist-info → hud_python-0.4.9.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.7.dist-info → hud_python-0.4.9.dist-info}/licenses/LICENSE +0 -0
hud/cli/remove.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Remove HUD environments from local registry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from hud.utils.design import HUDDesign
|
|
12
|
+
|
|
13
|
+
from .registry import get_registry_dir, list_registry_entries, load_from_registry
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def remove_environment(
|
|
17
|
+
target: str,
|
|
18
|
+
yes: bool = False,
|
|
19
|
+
verbose: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Remove an environment from the local registry."""
|
|
22
|
+
design = HUDDesign()
|
|
23
|
+
design.header("HUD Environment Removal")
|
|
24
|
+
|
|
25
|
+
# Find the environment to remove
|
|
26
|
+
found_entry = None
|
|
27
|
+
found_digest = None
|
|
28
|
+
|
|
29
|
+
# First check if target is a digest
|
|
30
|
+
for digest, lock_file in list_registry_entries():
|
|
31
|
+
if digest.startswith(target):
|
|
32
|
+
found_entry = lock_file
|
|
33
|
+
found_digest = digest
|
|
34
|
+
break
|
|
35
|
+
|
|
36
|
+
# If not found by digest, search by name
|
|
37
|
+
if not found_entry:
|
|
38
|
+
for digest, lock_file in list_registry_entries():
|
|
39
|
+
try:
|
|
40
|
+
lock_data = load_from_registry(digest)
|
|
41
|
+
if lock_data and "image" in lock_data:
|
|
42
|
+
image = lock_data["image"]
|
|
43
|
+
# Extract name and tag
|
|
44
|
+
name = image.split("@")[0] if "@" in image else image
|
|
45
|
+
if "/" in name:
|
|
46
|
+
# Match by repo/name or just name
|
|
47
|
+
if target in name or name.endswith(f"/{target}"):
|
|
48
|
+
found_entry = lock_file
|
|
49
|
+
found_digest = digest
|
|
50
|
+
break
|
|
51
|
+
except Exception:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if not found_entry:
|
|
55
|
+
design.error(f"Environment not found: {target}")
|
|
56
|
+
design.info("Use 'hud list' to see available environments")
|
|
57
|
+
raise typer.Exit(1)
|
|
58
|
+
|
|
59
|
+
# Load and display environment info
|
|
60
|
+
try:
|
|
61
|
+
lock_data = load_from_registry(found_digest)
|
|
62
|
+
if lock_data:
|
|
63
|
+
image = lock_data.get("image", "unknown")
|
|
64
|
+
metadata = lock_data.get("metadata", {})
|
|
65
|
+
description = metadata.get("description", "No description")
|
|
66
|
+
|
|
67
|
+
design.section_title("Environment Details")
|
|
68
|
+
design.status_item("Image", image)
|
|
69
|
+
design.status_item("Digest", found_digest)
|
|
70
|
+
design.status_item("Description", description)
|
|
71
|
+
design.status_item("Location", str(found_entry.parent))
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if verbose:
|
|
74
|
+
design.warning(f"Could not read environment details: {e}")
|
|
75
|
+
|
|
76
|
+
# Confirm deletion
|
|
77
|
+
if not yes:
|
|
78
|
+
design.info("")
|
|
79
|
+
if not typer.confirm(f"Remove environment {found_digest}?"):
|
|
80
|
+
design.info("Aborted")
|
|
81
|
+
raise typer.Exit(0)
|
|
82
|
+
|
|
83
|
+
# Remove the environment directory
|
|
84
|
+
try:
|
|
85
|
+
env_dir = found_entry.parent
|
|
86
|
+
shutil.rmtree(env_dir)
|
|
87
|
+
design.success(f"Removed environment: {found_digest}")
|
|
88
|
+
|
|
89
|
+
# Check if the image is still available locally
|
|
90
|
+
if lock_data:
|
|
91
|
+
image = lock_data.get("image", "")
|
|
92
|
+
if image:
|
|
93
|
+
design.info("")
|
|
94
|
+
design.info("Note: The Docker image may still exist locally.")
|
|
95
|
+
design.info(f"To remove it, run: [cyan]docker rmi {image.split('@')[0]}[/cyan]")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
design.error(f"Failed to remove environment: {e}")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def remove_all_environments(
|
|
102
|
+
yes: bool = False,
|
|
103
|
+
verbose: bool = False,
|
|
104
|
+
) -> None:
|
|
105
|
+
"""Remove all environments from the local registry."""
|
|
106
|
+
design = HUDDesign()
|
|
107
|
+
design.header("Remove All HUD Environments")
|
|
108
|
+
|
|
109
|
+
registry_dir = get_registry_dir()
|
|
110
|
+
if not registry_dir.exists():
|
|
111
|
+
design.info("No environments found in local registry.")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Count environments
|
|
115
|
+
entries = list(list_registry_entries())
|
|
116
|
+
if not entries:
|
|
117
|
+
design.info("No environments found in local registry.")
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
design.warning(f"This will remove {len(entries)} environment(s) from the local registry!")
|
|
121
|
+
|
|
122
|
+
# List environments that will be removed
|
|
123
|
+
design.section_title("Environments to Remove")
|
|
124
|
+
for digest, lock_file in entries:
|
|
125
|
+
try:
|
|
126
|
+
lock_data = load_from_registry(digest)
|
|
127
|
+
if lock_data:
|
|
128
|
+
image = lock_data.get("image", "unknown")
|
|
129
|
+
design.info(f" • {digest[:12]} - {image}")
|
|
130
|
+
except Exception:
|
|
131
|
+
design.info(f" • {digest[:12]}")
|
|
132
|
+
|
|
133
|
+
# Confirm deletion
|
|
134
|
+
if not yes:
|
|
135
|
+
design.info("")
|
|
136
|
+
if not typer.confirm("Remove ALL environments?", default=False):
|
|
137
|
+
design.info("Aborted")
|
|
138
|
+
raise typer.Exit(0)
|
|
139
|
+
|
|
140
|
+
# Remove all environments
|
|
141
|
+
removed = 0
|
|
142
|
+
failed = 0
|
|
143
|
+
|
|
144
|
+
for digest, lock_file in entries:
|
|
145
|
+
try:
|
|
146
|
+
env_dir = lock_file.parent
|
|
147
|
+
shutil.rmtree(env_dir)
|
|
148
|
+
removed += 1
|
|
149
|
+
if verbose:
|
|
150
|
+
design.success(f"Removed: {digest}")
|
|
151
|
+
except Exception as e:
|
|
152
|
+
failed += 1
|
|
153
|
+
if verbose:
|
|
154
|
+
design.error(f"Failed to remove {digest}: {e}")
|
|
155
|
+
|
|
156
|
+
design.info("")
|
|
157
|
+
if failed == 0:
|
|
158
|
+
design.success(f"Successfully removed {removed} environment(s)")
|
|
159
|
+
else:
|
|
160
|
+
design.warning(f"Removed {removed} environment(s), failed to remove {failed}")
|
|
161
|
+
|
|
162
|
+
design.info("")
|
|
163
|
+
design.info("Note: Docker images may still exist locally.")
|
|
164
|
+
design.info("To remove them, use: [cyan]docker image prune[/cyan]")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def remove_command(
|
|
168
|
+
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"
|
|
177
|
+
),
|
|
178
|
+
) -> None:
|
|
179
|
+
"""🗑️ Remove HUD environments from local registry.
|
|
180
|
+
|
|
181
|
+
Removes environment metadata from ~/.hud/envs/
|
|
182
|
+
Note: This does not remove the Docker images.
|
|
183
|
+
|
|
184
|
+
Examples:
|
|
185
|
+
hud remove abc123 # Remove by digest
|
|
186
|
+
hud remove text_2048 # Remove by name
|
|
187
|
+
hud remove hudpython/test_init # Remove by full name
|
|
188
|
+
hud remove all # Remove all environments
|
|
189
|
+
hud remove all --yes # Remove all without confirmation
|
|
190
|
+
"""
|
|
191
|
+
if not target:
|
|
192
|
+
design = HUDDesign()
|
|
193
|
+
design.error("Please specify an environment to remove or 'all'")
|
|
194
|
+
design.info("Use 'hud list' to see available environments")
|
|
195
|
+
raise typer.Exit(1)
|
|
196
|
+
|
|
197
|
+
if target.lower() == "all":
|
|
198
|
+
remove_all_environments(yes, verbose)
|
|
199
|
+
else:
|
|
200
|
+
remove_environment(target, yes, verbose)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Tests for analyze_metadata.py - Fast metadata analysis functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from hud.cli.analyze_metadata import (
|
|
15
|
+
analyze_from_metadata,
|
|
16
|
+
check_local_cache,
|
|
17
|
+
fetch_lock_from_registry,
|
|
18
|
+
)
|
|
19
|
+
from hud.cli.registry import save_to_registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def mock_registry_dir(tmp_path):
|
|
24
|
+
"""Create a mock registry directory."""
|
|
25
|
+
registry_dir = tmp_path / ".hud" / "envs"
|
|
26
|
+
registry_dir.mkdir(parents=True)
|
|
27
|
+
return registry_dir
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def sample_lock_data():
|
|
32
|
+
"""Sample lock data for testing."""
|
|
33
|
+
return {
|
|
34
|
+
"image": "test/environment:latest",
|
|
35
|
+
"digest": "sha256:abc123",
|
|
36
|
+
"build": {
|
|
37
|
+
"timestamp": 1234567890,
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"hud_version": "0.1.0",
|
|
40
|
+
},
|
|
41
|
+
"environment": {
|
|
42
|
+
"initializeMs": 1500,
|
|
43
|
+
"toolCount": 5,
|
|
44
|
+
"variables": {"API_KEY": "required"},
|
|
45
|
+
},
|
|
46
|
+
"tools": [
|
|
47
|
+
{
|
|
48
|
+
"name": "test_tool",
|
|
49
|
+
"description": "A test tool",
|
|
50
|
+
"inputSchema": {
|
|
51
|
+
"type": "object",
|
|
52
|
+
"properties": {"message": {"type": "string"}},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"resources": [
|
|
57
|
+
{
|
|
58
|
+
"uri": "test://resource",
|
|
59
|
+
"name": "Test Resource",
|
|
60
|
+
"description": "A test resource",
|
|
61
|
+
"mimeType": "text/plain",
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"prompts": [
|
|
65
|
+
{
|
|
66
|
+
"name": "test_prompt",
|
|
67
|
+
"description": "A test prompt",
|
|
68
|
+
"arguments": [{"name": "arg1", "description": "First argument"}],
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TestFetchLockFromRegistry:
|
|
75
|
+
"""Test fetching lock data from registry."""
|
|
76
|
+
|
|
77
|
+
@mock.patch("requests.get")
|
|
78
|
+
def test_fetch_lock_success(self, mock_get):
|
|
79
|
+
"""Test successful fetch from registry."""
|
|
80
|
+
mock_response = mock.Mock()
|
|
81
|
+
mock_response.status_code = 200
|
|
82
|
+
mock_response.json.return_value = {
|
|
83
|
+
"lock": yaml.dump({"test": "data"})
|
|
84
|
+
}
|
|
85
|
+
mock_get.return_value = mock_response
|
|
86
|
+
|
|
87
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
88
|
+
assert result == {"test": "data"}
|
|
89
|
+
mock_get.assert_called_once()
|
|
90
|
+
|
|
91
|
+
@mock.patch("requests.get")
|
|
92
|
+
def test_fetch_lock_with_lock_data(self, mock_get):
|
|
93
|
+
"""Test fetch when response has lock_data key."""
|
|
94
|
+
mock_response = mock.Mock()
|
|
95
|
+
mock_response.status_code = 200
|
|
96
|
+
mock_response.json.return_value = {
|
|
97
|
+
"lock_data": {"test": "data"}
|
|
98
|
+
}
|
|
99
|
+
mock_get.return_value = mock_response
|
|
100
|
+
|
|
101
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
102
|
+
assert result == {"test": "data"}
|
|
103
|
+
|
|
104
|
+
@mock.patch("requests.get")
|
|
105
|
+
def test_fetch_lock_direct_data(self, mock_get):
|
|
106
|
+
"""Test fetch when response is direct lock data."""
|
|
107
|
+
mock_response = mock.Mock()
|
|
108
|
+
mock_response.status_code = 200
|
|
109
|
+
mock_response.json.return_value = {"test": "data"}
|
|
110
|
+
mock_get.return_value = mock_response
|
|
111
|
+
|
|
112
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
113
|
+
assert result == {"test": "data"}
|
|
114
|
+
|
|
115
|
+
@mock.patch("requests.get")
|
|
116
|
+
def test_fetch_lock_adds_latest_tag(self, mock_get):
|
|
117
|
+
"""Test that :latest tag is added if missing."""
|
|
118
|
+
mock_response = mock.Mock()
|
|
119
|
+
mock_response.status_code = 404
|
|
120
|
+
mock_get.return_value = mock_response
|
|
121
|
+
|
|
122
|
+
fetch_lock_from_registry("test/env")
|
|
123
|
+
|
|
124
|
+
# Check that the URL includes :latest
|
|
125
|
+
call_args = mock_get.call_args
|
|
126
|
+
assert "test/env:latest" in call_args[0][0]
|
|
127
|
+
|
|
128
|
+
@mock.patch("requests.get")
|
|
129
|
+
def test_fetch_lock_failure(self, mock_get):
|
|
130
|
+
"""Test fetch failure returns None."""
|
|
131
|
+
mock_response = mock.Mock()
|
|
132
|
+
mock_response.status_code = 404
|
|
133
|
+
mock_get.return_value = mock_response
|
|
134
|
+
|
|
135
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
136
|
+
assert result is None
|
|
137
|
+
|
|
138
|
+
@mock.patch("requests.get")
|
|
139
|
+
def test_fetch_lock_exception(self, mock_get):
|
|
140
|
+
"""Test fetch exception returns None."""
|
|
141
|
+
mock_get.side_effect = Exception("Network error")
|
|
142
|
+
|
|
143
|
+
result = fetch_lock_from_registry("test/env:latest")
|
|
144
|
+
assert result is None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestCheckLocalCache:
|
|
148
|
+
"""Test checking local cache for lock data."""
|
|
149
|
+
|
|
150
|
+
def test_check_local_cache_found(self, mock_registry_dir, sample_lock_data, monkeypatch):
|
|
151
|
+
"""Test finding lock data in local cache."""
|
|
152
|
+
# Mock registry directory
|
|
153
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
154
|
+
|
|
155
|
+
# Save sample data to registry
|
|
156
|
+
save_to_registry(sample_lock_data, "test/environment:latest", verbose=False)
|
|
157
|
+
|
|
158
|
+
# Check cache
|
|
159
|
+
result = check_local_cache("test/environment:latest")
|
|
160
|
+
assert result is not None
|
|
161
|
+
assert result["image"] == "test/environment:latest"
|
|
162
|
+
|
|
163
|
+
def test_check_local_cache_not_found(self, mock_registry_dir, monkeypatch):
|
|
164
|
+
"""Test when lock data not in local cache."""
|
|
165
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
166
|
+
|
|
167
|
+
result = check_local_cache("nonexistent/env:latest")
|
|
168
|
+
assert result is None
|
|
169
|
+
|
|
170
|
+
def test_check_local_cache_invalid_yaml(self, mock_registry_dir, monkeypatch):
|
|
171
|
+
"""Test when lock file has invalid YAML."""
|
|
172
|
+
monkeypatch.setattr("hud.cli.registry.get_registry_dir", lambda: mock_registry_dir)
|
|
173
|
+
|
|
174
|
+
# Create invalid lock file
|
|
175
|
+
digest = "sha256:invalid"
|
|
176
|
+
lock_file = mock_registry_dir / digest / "hud.lock.yaml"
|
|
177
|
+
lock_file.parent.mkdir(parents=True)
|
|
178
|
+
lock_file.write_text("invalid: yaml: content:")
|
|
179
|
+
|
|
180
|
+
result = check_local_cache("test/invalid:latest")
|
|
181
|
+
assert result is None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Note: TestFormatAnalysisOutput class removed since format_analysis_output function doesn't exist
|
|
185
|
+
# The formatting is done inline within analyze_from_metadata
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
class TestAnalyzeFromMetadata:
|
|
190
|
+
"""Test the main analyze_from_metadata function."""
|
|
191
|
+
|
|
192
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
193
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
194
|
+
async def test_analyze_from_local_cache(self, mock_console, mock_check, sample_lock_data):
|
|
195
|
+
"""Test analyzing from local cache."""
|
|
196
|
+
mock_check.return_value = sample_lock_data
|
|
197
|
+
|
|
198
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
199
|
+
|
|
200
|
+
mock_check.assert_called_once_with("test/env:latest")
|
|
201
|
+
# Should output JSON
|
|
202
|
+
mock_console.print_json.assert_called_once()
|
|
203
|
+
|
|
204
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
205
|
+
@mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
|
|
206
|
+
@mock.patch("hud.cli.analyze_metadata.save_to_registry")
|
|
207
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
208
|
+
async def test_analyze_from_registry(
|
|
209
|
+
self, mock_console, mock_save, mock_fetch, mock_check, sample_lock_data
|
|
210
|
+
):
|
|
211
|
+
"""Test analyzing from registry when not in cache."""
|
|
212
|
+
mock_check.return_value = None
|
|
213
|
+
mock_fetch.return_value = sample_lock_data
|
|
214
|
+
|
|
215
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=False)
|
|
216
|
+
|
|
217
|
+
mock_check.assert_called_once()
|
|
218
|
+
mock_fetch.assert_called_once()
|
|
219
|
+
mock_save.assert_called_once() # Should save to cache
|
|
220
|
+
mock_console.print_json.assert_called_once()
|
|
221
|
+
|
|
222
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
223
|
+
@mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
|
|
224
|
+
@mock.patch("hud.cli.analyze_metadata.design")
|
|
225
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
226
|
+
async def test_analyze_not_found(self, mock_console, mock_design, mock_fetch, mock_check):
|
|
227
|
+
"""Test when environment not found anywhere."""
|
|
228
|
+
mock_check.return_value = None
|
|
229
|
+
mock_fetch.return_value = None
|
|
230
|
+
|
|
231
|
+
await analyze_from_metadata("test/notfound:latest", "json", verbose=False)
|
|
232
|
+
|
|
233
|
+
# Should show error
|
|
234
|
+
mock_design.error.assert_called_with("Environment metadata not found")
|
|
235
|
+
# Should print suggestions
|
|
236
|
+
mock_console.print.assert_called()
|
|
237
|
+
|
|
238
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
239
|
+
@mock.patch("hud.cli.analyze_metadata.console")
|
|
240
|
+
async def test_analyze_verbose_mode(self, mock_console, mock_check, sample_lock_data):
|
|
241
|
+
"""Test verbose mode includes input schemas."""
|
|
242
|
+
mock_check.return_value = sample_lock_data
|
|
243
|
+
|
|
244
|
+
await analyze_from_metadata("test/env:latest", "json", verbose=True)
|
|
245
|
+
|
|
246
|
+
# In verbose mode, the JSON output should include input schemas
|
|
247
|
+
mock_console.print_json.assert_called_once()
|
|
248
|
+
# Get the JSON string that was printed
|
|
249
|
+
call_args = mock_console.print_json.call_args[0][0]
|
|
250
|
+
output_data = json.loads(call_args)
|
|
251
|
+
assert "inputSchema" in output_data["tools"][0]
|
|
252
|
+
|
|
253
|
+
@mock.patch("hud.cli.analyze_metadata.check_local_cache")
|
|
254
|
+
@mock.patch("hud.cli.analyze_metadata.fetch_lock_from_registry")
|
|
255
|
+
async def test_analyze_registry_reference_parsing(self, mock_fetch, mock_check):
|
|
256
|
+
"""Test parsing of different registry reference formats."""
|
|
257
|
+
mock_check.return_value = None
|
|
258
|
+
mock_fetch.return_value = {"test": "data"}
|
|
259
|
+
|
|
260
|
+
# Test different reference formats
|
|
261
|
+
test_cases = [
|
|
262
|
+
("docker.io/org/name:tag", "org/name:tag"),
|
|
263
|
+
("registry-1.docker.io/org/name", "org/name"),
|
|
264
|
+
("org/name@sha256:abc", "org/name"),
|
|
265
|
+
("org/name", "org/name"),
|
|
266
|
+
("name:tag", "name:tag"),
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
for input_ref, expected_call in test_cases:
|
|
270
|
+
await analyze_from_metadata(input_ref, "json", verbose=False)
|
|
271
|
+
|
|
272
|
+
# Check what was passed to fetch_lock_from_registry
|
|
273
|
+
calls = mock_fetch.call_args_list
|
|
274
|
+
last_call = calls[-1][0][0]
|
|
275
|
+
|
|
276
|
+
# The function might add :latest, so check base name
|
|
277
|
+
assert expected_call.split(":")[0] in last_call
|