hud-python 0.4.8__py3-none-any.whl → 0.4.10__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.

@@ -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