hud-python 0.4.10__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.

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 +106 -51
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +60 -25
  9. hud/cli/eval.py +148 -68
  10. hud/cli/init.py +0 -1
  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 +7 -5
  38. hud/clients/mcp_use.py +8 -6
  39. hud/server/server.py +34 -4
  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 +15 -4
  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.10.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
  57. {hud_python-0.4.10.dist-info → hud_python-0.4.12.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.10.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.10.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
@@ -3,9 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import subprocess
6
- import tempfile
7
- from datetime import datetime
8
- from pathlib import Path
9
6
  from unittest import mock
10
7
 
11
8
  import pytest
@@ -81,11 +78,7 @@ class TestGetExistingVersion:
81
78
 
82
79
  def test_get_version_from_lock(self, tmp_path):
83
80
  """Test extracting version from lock file."""
84
- lock_data = {
85
- "build": {
86
- "version": "1.2.3"
87
- }
88
- }
81
+ lock_data = {"build": {"version": "1.2.3"}}
89
82
  lock_path = tmp_path / "hud.lock.yaml"
90
83
  lock_path.write_text(yaml.dump(lock_data))
91
84
 
@@ -119,8 +112,7 @@ class TestGetDockerImageDigest:
119
112
  """Test successfully getting image digest."""
120
113
  # Note: The function expects to parse a list from the string representation
121
114
  mock_run.return_value = mock.Mock(
122
- stdout="['docker.io/library/test@sha256:abc123']",
123
- returncode=0
115
+ stdout="['docker.io/library/test@sha256:abc123']", returncode=0
124
116
  )
125
117
 
126
118
  result = get_docker_image_digest("test:latest")
@@ -149,10 +141,7 @@ class TestGetDockerImageId:
149
141
  @mock.patch("subprocess.run")
150
142
  def test_get_id_success(self, mock_run):
151
143
  """Test successfully getting image ID."""
152
- mock_run.return_value = mock.Mock(
153
- stdout="sha256:abc123def456",
154
- returncode=0
155
- )
144
+ mock_run.return_value = mock.Mock(stdout="sha256:abc123def456", returncode=0)
156
145
 
157
146
  result = get_docker_image_id("test:latest")
158
147
  assert result == "sha256:abc123def456"
@@ -223,13 +212,13 @@ class TestAnalyzeMcpEnvironment:
223
212
  # Setup mock client
224
213
  mock_client = mock.AsyncMock()
225
214
  mock_client_class.return_value = mock_client
226
-
215
+
227
216
  # Mock tool
228
217
  mock_tool = mock.Mock()
229
218
  mock_tool.name = "test_tool"
230
219
  mock_tool.description = "Test tool"
231
220
  mock_tool.inputSchema = {"type": "object"}
232
-
221
+
233
222
  mock_client.list_tools.return_value = [mock_tool]
234
223
 
235
224
  result = await analyze_mcp_environment("test:latest")
@@ -264,7 +253,7 @@ class TestAnalyzeMcpEnvironment:
264
253
 
265
254
  # Just test that it runs without error in verbose mode
266
255
  result = await analyze_mcp_environment("test:latest", verbose=True)
267
-
256
+
268
257
  assert result["success"] is True
269
258
  assert "initializeMs" in result
270
259
 
@@ -272,35 +261,31 @@ class TestAnalyzeMcpEnvironment:
272
261
  class TestBuildDockerImage:
273
262
  """Test building Docker images."""
274
263
 
275
- @mock.patch("subprocess.Popen")
276
- def test_build_success(self, mock_popen, tmp_path):
264
+ @mock.patch("subprocess.run")
265
+ def test_build_success(self, mock_run, tmp_path):
277
266
  """Test successful Docker build."""
278
267
  # Create Dockerfile
279
268
  dockerfile = tmp_path / "Dockerfile"
280
269
  dockerfile.write_text("FROM python:3.11")
281
270
 
282
271
  # Mock successful process
283
- mock_process = mock.Mock()
284
- mock_process.stdout = ["Step 1/1 : FROM python:3.11\n", "Successfully built abc123\n"]
285
- mock_process.wait.return_value = None
286
- mock_process.returncode = 0
287
- mock_popen.return_value = mock_process
272
+ mock_result = mock.Mock()
273
+ mock_result.returncode = 0
274
+ mock_run.return_value = mock_result
288
275
 
289
276
  result = build_docker_image(tmp_path, "test:latest")
290
277
  assert result is True
291
278
 
292
- @mock.patch("subprocess.Popen")
293
- def test_build_failure(self, mock_popen, tmp_path):
279
+ @mock.patch("subprocess.run")
280
+ def test_build_failure(self, mock_run, tmp_path):
294
281
  """Test failed Docker build."""
295
282
  dockerfile = tmp_path / "Dockerfile"
296
283
  dockerfile.write_text("FROM python:3.11")
297
284
 
298
285
  # Mock failed process
299
- mock_process = mock.Mock()
300
- mock_process.stdout = ["Error: failed to build\n"]
301
- mock_process.wait.return_value = None
302
- mock_process.returncode = 1
303
- mock_popen.return_value = mock_process
286
+ mock_result = mock.Mock()
287
+ mock_result.returncode = 1
288
+ mock_run.return_value = mock_result
304
289
 
305
290
  result = build_docker_image(tmp_path, "test:latest")
306
291
  assert result is False
@@ -310,22 +295,20 @@ class TestBuildDockerImage:
310
295
  result = build_docker_image(tmp_path, "test:latest")
311
296
  assert result is False
312
297
 
313
- @mock.patch("subprocess.Popen")
314
- def test_build_with_no_cache(self, mock_popen, tmp_path):
298
+ @mock.patch("subprocess.run")
299
+ def test_build_with_no_cache(self, mock_run, tmp_path):
315
300
  """Test build with --no-cache flag."""
316
301
  dockerfile = tmp_path / "Dockerfile"
317
302
  dockerfile.write_text("FROM python:3.11")
318
303
 
319
- mock_process = mock.Mock()
320
- mock_process.stdout = []
321
- mock_process.wait.return_value = None
322
- mock_process.returncode = 0
323
- mock_popen.return_value = mock_process
304
+ mock_result = mock.Mock()
305
+ mock_result.returncode = 0
306
+ mock_run.return_value = mock_result
324
307
 
325
308
  build_docker_image(tmp_path, "test:latest", no_cache=True)
326
309
 
327
310
  # Check that --no-cache was included
328
- call_args = mock_popen.call_args[0][0]
311
+ call_args = mock_run.call_args[0][0]
329
312
  assert "--no-cache" in call_args
330
313
 
331
314
 
@@ -336,12 +319,10 @@ class TestBuildEnvironment:
336
319
  @mock.patch("hud.cli.build.analyze_mcp_environment")
337
320
  @mock.patch("hud.cli.build.save_to_registry")
338
321
  @mock.patch("hud.cli.build.get_docker_image_id")
339
- @mock.patch("subprocess.Popen")
340
322
  @mock.patch("subprocess.run")
341
323
  def test_build_environment_success(
342
324
  self,
343
325
  mock_run,
344
- mock_popen,
345
326
  mock_get_id,
346
327
  mock_save_registry,
347
328
  mock_analyze,
@@ -352,14 +333,14 @@ class TestBuildEnvironment:
352
333
  # Setup directory structure
353
334
  env_dir = tmp_path / "test-env"
354
335
  env_dir.mkdir()
355
-
336
+
356
337
  # Create pyproject.toml
357
338
  pyproject = env_dir / "pyproject.toml"
358
339
  pyproject.write_text("""
359
340
  [tool.hud]
360
341
  image = "test/env:dev"
361
342
  """)
362
-
343
+
363
344
  # Create Dockerfile
364
345
  dockerfile = env_dir / "Dockerfile"
365
346
  dockerfile.write_text("""
@@ -379,16 +360,11 @@ ENV API_KEY
379
360
  ],
380
361
  }
381
362
  mock_get_id.return_value = "sha256:abc123"
382
-
363
+
383
364
  # Mock final rebuild
384
- mock_process = mock.Mock()
385
- # Create a mock file-like object with read method
386
- mock_stdout = mock.Mock()
387
- mock_stdout.read.return_value = ""
388
- mock_process.stdout = mock_stdout
389
- mock_process.wait.return_value = None
390
- mock_process.returncode = 0
391
- mock_popen.return_value = mock_process
365
+ mock_result = mock.Mock()
366
+ mock_result.returncode = 0
367
+ mock_run.return_value = mock_result
392
368
 
393
369
  # Run build
394
370
  build_environment(str(env_dir), "test/env:latest")
@@ -9,7 +9,7 @@ from unittest.mock import mock_open, patch
9
9
 
10
10
  import pytest
11
11
 
12
- from hud.cli.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
12
+ from hud.cli.utils.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
13
13
 
14
14
 
15
15
  class TestParseCursorConfig:
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
8
8
  import pytest
9
9
 
10
10
  from hud.cli.debug import debug_mcp_stdio
11
- from hud.cli.utils import CaptureLogger
11
+ from hud.cli.utils.logging import CaptureLogger
12
12
 
13
13
 
14
14
  class TestDebugMCPStdio:
@@ -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
  ):