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.

Files changed (63) hide show
  1. hud/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +114 -52
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +101 -38
  9. hud/cli/eval.py +175 -90
  10. hud/cli/init.py +442 -64
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +5 -13
  38. hud/clients/mcp_use.py +6 -10
  39. hud/server/server.py +35 -5
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/METADATA +16 -13
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -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, list_environments, list_command
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(yaml.dump({
105
- "image": "test/env1:latest",
106
- "metadata": {
107
- "description": "Test environment 1",
108
- "tools": ["tool1", "tool2", "tool3"]
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(yaml.dump({
119
- "image": "test/env2:v1.0",
120
- "metadata": {
121
- "description": "Test environment 2 with a much longer description that should be truncated",
122
- "tools": ["tool1"]
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(self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys):
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(self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys):
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(self, mock_list_entries, mock_get_registry, mock_registry_dir):
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(self, mock_list_entries, mock_get_registry, mock_registry_dir, capsys):
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(self, mock_list_entries, mock_get_registry, mock_registry_dir):
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
@@ -1,4 +1,4 @@
1
- """Tests for hud.cli.mcp_server module."""
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.mcp_server import (
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.mcp_server.image_exists", return_value=False),
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
  ):
@@ -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
- "layers": [
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:latest" in call_args[0][0]
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
- "generatedAt": "2024-01-01T00:00:00Z",
214
- "hudVersion": "1.0.0"
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(self, mock_popen, mock_fetch, mock_manifest, mock_design_class):
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
  )