hud-python 0.4.8__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/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.8.dist-info → hud_python-0.4.9.dist-info}/METADATA +12 -1
- {hud_python-0.4.8.dist-info → hud_python-0.4.9.dist-info}/RECORD +29 -18
- {hud_python-0.4.8.dist-info → hud_python-0.4.9.dist-info}/WHEEL +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.9.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.8.dist-info → hud_python-0.4.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""Tests for build.py - Build HUD environments and generate lock files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest import mock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import typer
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from hud.cli.build import (
|
|
16
|
+
analyze_mcp_environment,
|
|
17
|
+
build_docker_image,
|
|
18
|
+
build_environment,
|
|
19
|
+
extract_env_vars_from_dockerfile,
|
|
20
|
+
get_docker_image_digest,
|
|
21
|
+
get_docker_image_id,
|
|
22
|
+
get_existing_version,
|
|
23
|
+
increment_version,
|
|
24
|
+
parse_version,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestParseVersion:
|
|
29
|
+
"""Test version parsing functionality."""
|
|
30
|
+
|
|
31
|
+
def test_parse_standard_version(self):
|
|
32
|
+
"""Test parsing standard semantic version."""
|
|
33
|
+
assert parse_version("1.2.3") == (1, 2, 3)
|
|
34
|
+
assert parse_version("10.20.30") == (10, 20, 30)
|
|
35
|
+
|
|
36
|
+
def test_parse_version_with_v_prefix(self):
|
|
37
|
+
"""Test parsing version with v prefix."""
|
|
38
|
+
assert parse_version("v1.2.3") == (1, 2, 3)
|
|
39
|
+
assert parse_version("v2.0.0") == (2, 0, 0)
|
|
40
|
+
|
|
41
|
+
def test_parse_incomplete_version(self):
|
|
42
|
+
"""Test parsing versions with missing parts."""
|
|
43
|
+
assert parse_version("1.2") == (1, 2, 0)
|
|
44
|
+
assert parse_version("1") == (1, 0, 0)
|
|
45
|
+
assert parse_version("") == (0, 0, 0)
|
|
46
|
+
|
|
47
|
+
def test_parse_invalid_version(self):
|
|
48
|
+
"""Test parsing invalid versions."""
|
|
49
|
+
assert parse_version("abc") == (0, 0, 0)
|
|
50
|
+
assert parse_version("1.x.3") == (0, 0, 0)
|
|
51
|
+
assert parse_version("not-a-version") == (0, 0, 0)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TestIncrementVersion:
|
|
55
|
+
"""Test version incrementing functionality."""
|
|
56
|
+
|
|
57
|
+
def test_increment_patch(self):
|
|
58
|
+
"""Test incrementing patch version."""
|
|
59
|
+
assert increment_version("1.2.3") == "1.2.4"
|
|
60
|
+
assert increment_version("1.2.3", "patch") == "1.2.4"
|
|
61
|
+
assert increment_version("1.0.0") == "1.0.1"
|
|
62
|
+
|
|
63
|
+
def test_increment_minor(self):
|
|
64
|
+
"""Test incrementing minor version."""
|
|
65
|
+
assert increment_version("1.2.3", "minor") == "1.3.0"
|
|
66
|
+
assert increment_version("0.5.10", "minor") == "0.6.0"
|
|
67
|
+
|
|
68
|
+
def test_increment_major(self):
|
|
69
|
+
"""Test incrementing major version."""
|
|
70
|
+
assert increment_version("1.2.3", "major") == "2.0.0"
|
|
71
|
+
assert increment_version("0.5.10", "major") == "1.0.0"
|
|
72
|
+
|
|
73
|
+
def test_increment_with_v_prefix(self):
|
|
74
|
+
"""Test incrementing version with v prefix."""
|
|
75
|
+
assert increment_version("v1.2.3") == "1.2.4"
|
|
76
|
+
assert increment_version("v2.0.0", "major") == "3.0.0"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestGetExistingVersion:
|
|
80
|
+
"""Test getting version from lock file."""
|
|
81
|
+
|
|
82
|
+
def test_get_version_from_lock(self, tmp_path):
|
|
83
|
+
"""Test extracting version from lock file."""
|
|
84
|
+
lock_data = {
|
|
85
|
+
"build": {
|
|
86
|
+
"version": "1.2.3"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lock_path = tmp_path / "hud.lock.yaml"
|
|
90
|
+
lock_path.write_text(yaml.dump(lock_data))
|
|
91
|
+
|
|
92
|
+
assert get_existing_version(lock_path) == "1.2.3"
|
|
93
|
+
|
|
94
|
+
def test_get_version_no_build_section(self, tmp_path):
|
|
95
|
+
"""Test when lock file has no build section."""
|
|
96
|
+
lock_data = {"other": "data"}
|
|
97
|
+
lock_path = tmp_path / "hud.lock.yaml"
|
|
98
|
+
lock_path.write_text(yaml.dump(lock_data))
|
|
99
|
+
|
|
100
|
+
assert get_existing_version(lock_path) is None
|
|
101
|
+
|
|
102
|
+
def test_get_version_no_file(self, tmp_path):
|
|
103
|
+
"""Test when lock file doesn't exist."""
|
|
104
|
+
lock_path = tmp_path / "hud.lock.yaml"
|
|
105
|
+
assert get_existing_version(lock_path) is None
|
|
106
|
+
|
|
107
|
+
def test_get_version_invalid_yaml(self, tmp_path):
|
|
108
|
+
"""Test when lock file has invalid YAML."""
|
|
109
|
+
lock_path = tmp_path / "hud.lock.yaml"
|
|
110
|
+
lock_path.write_text("invalid: yaml: content:")
|
|
111
|
+
assert get_existing_version(lock_path) is None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestGetDockerImageDigest:
|
|
115
|
+
"""Test getting Docker image digest."""
|
|
116
|
+
|
|
117
|
+
@mock.patch("subprocess.run")
|
|
118
|
+
def test_get_digest_success(self, mock_run):
|
|
119
|
+
"""Test successfully getting image digest."""
|
|
120
|
+
# Note: The function expects to parse a list from the string representation
|
|
121
|
+
mock_run.return_value = mock.Mock(
|
|
122
|
+
stdout="['docker.io/library/test@sha256:abc123']",
|
|
123
|
+
returncode=0
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
result = get_docker_image_digest("test:latest")
|
|
127
|
+
assert result == "docker.io/library/test@sha256:abc123"
|
|
128
|
+
|
|
129
|
+
@mock.patch("subprocess.run")
|
|
130
|
+
def test_get_digest_empty(self, mock_run):
|
|
131
|
+
"""Test when docker returns empty digest list."""
|
|
132
|
+
mock_run.return_value = mock.Mock(stdout="[]", returncode=0)
|
|
133
|
+
|
|
134
|
+
result = get_docker_image_digest("test:latest")
|
|
135
|
+
assert result is None
|
|
136
|
+
|
|
137
|
+
@mock.patch("subprocess.run")
|
|
138
|
+
def test_get_digest_failure(self, mock_run):
|
|
139
|
+
"""Test when docker command fails."""
|
|
140
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, ["docker"])
|
|
141
|
+
|
|
142
|
+
result = get_docker_image_digest("test:latest")
|
|
143
|
+
assert result is None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestGetDockerImageId:
|
|
147
|
+
"""Test getting Docker image ID."""
|
|
148
|
+
|
|
149
|
+
@mock.patch("subprocess.run")
|
|
150
|
+
def test_get_id_success(self, mock_run):
|
|
151
|
+
"""Test successfully getting image ID."""
|
|
152
|
+
mock_run.return_value = mock.Mock(
|
|
153
|
+
stdout="sha256:abc123def456",
|
|
154
|
+
returncode=0
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
result = get_docker_image_id("test:latest")
|
|
158
|
+
assert result == "sha256:abc123def456"
|
|
159
|
+
|
|
160
|
+
@mock.patch("subprocess.run")
|
|
161
|
+
def test_get_id_empty(self, mock_run):
|
|
162
|
+
"""Test when docker returns empty ID."""
|
|
163
|
+
mock_run.return_value = mock.Mock(stdout="", returncode=0)
|
|
164
|
+
|
|
165
|
+
result = get_docker_image_id("test:latest")
|
|
166
|
+
assert result is None
|
|
167
|
+
|
|
168
|
+
@mock.patch("subprocess.run")
|
|
169
|
+
def test_get_id_failure(self, mock_run):
|
|
170
|
+
"""Test when docker command fails."""
|
|
171
|
+
mock_run.side_effect = subprocess.CalledProcessError(1, ["docker"])
|
|
172
|
+
|
|
173
|
+
result = get_docker_image_id("test:latest")
|
|
174
|
+
assert result is None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class TestExtractEnvVarsFromDockerfile:
|
|
178
|
+
"""Test extracting environment variables from Dockerfile."""
|
|
179
|
+
|
|
180
|
+
def test_extract_required_env_vars(self, tmp_path):
|
|
181
|
+
"""Test extracting required environment variables."""
|
|
182
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
183
|
+
dockerfile.write_text("""
|
|
184
|
+
FROM python:3.11
|
|
185
|
+
ENV API_KEY
|
|
186
|
+
ENV SECRET_TOKEN=
|
|
187
|
+
ENV OTHER_VAR=default_value
|
|
188
|
+
""")
|
|
189
|
+
|
|
190
|
+
required, optional = extract_env_vars_from_dockerfile(dockerfile)
|
|
191
|
+
assert "API_KEY" in required
|
|
192
|
+
assert "SECRET_TOKEN" in required
|
|
193
|
+
assert "OTHER_VAR" not in required
|
|
194
|
+
assert len(optional) == 0
|
|
195
|
+
|
|
196
|
+
def test_extract_no_env_vars(self, tmp_path):
|
|
197
|
+
"""Test Dockerfile with no ENV directives."""
|
|
198
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
199
|
+
dockerfile.write_text("""
|
|
200
|
+
FROM python:3.11
|
|
201
|
+
RUN pip install fastmcp
|
|
202
|
+
""")
|
|
203
|
+
|
|
204
|
+
required, optional = extract_env_vars_from_dockerfile(dockerfile)
|
|
205
|
+
assert len(required) == 0
|
|
206
|
+
assert len(optional) == 0
|
|
207
|
+
|
|
208
|
+
def test_extract_no_dockerfile(self, tmp_path):
|
|
209
|
+
"""Test when Dockerfile doesn't exist."""
|
|
210
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
211
|
+
required, optional = extract_env_vars_from_dockerfile(dockerfile)
|
|
212
|
+
assert len(required) == 0
|
|
213
|
+
assert len(optional) == 0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.mark.asyncio
|
|
217
|
+
class TestAnalyzeMcpEnvironment:
|
|
218
|
+
"""Test analyzing MCP environment."""
|
|
219
|
+
|
|
220
|
+
@mock.patch("hud.cli.build.MCPClient")
|
|
221
|
+
async def test_analyze_success(self, mock_client_class):
|
|
222
|
+
"""Test successful environment analysis."""
|
|
223
|
+
# Setup mock client
|
|
224
|
+
mock_client = mock.AsyncMock()
|
|
225
|
+
mock_client_class.return_value = mock_client
|
|
226
|
+
|
|
227
|
+
# Mock tool
|
|
228
|
+
mock_tool = mock.Mock()
|
|
229
|
+
mock_tool.name = "test_tool"
|
|
230
|
+
mock_tool.description = "Test tool"
|
|
231
|
+
mock_tool.inputSchema = {"type": "object"}
|
|
232
|
+
|
|
233
|
+
mock_client.list_tools.return_value = [mock_tool]
|
|
234
|
+
|
|
235
|
+
result = await analyze_mcp_environment("test:latest")
|
|
236
|
+
|
|
237
|
+
assert result["success"] is True
|
|
238
|
+
assert result["toolCount"] == 1
|
|
239
|
+
assert len(result["tools"]) == 1
|
|
240
|
+
assert result["tools"][0]["name"] == "test_tool"
|
|
241
|
+
assert "initializeMs" in result
|
|
242
|
+
|
|
243
|
+
@mock.patch("hud.cli.build.MCPClient")
|
|
244
|
+
async def test_analyze_failure(self, mock_client_class):
|
|
245
|
+
"""Test failed environment analysis."""
|
|
246
|
+
# Setup mock client to fail
|
|
247
|
+
mock_client = mock.AsyncMock()
|
|
248
|
+
mock_client_class.return_value = mock_client
|
|
249
|
+
mock_client.initialize.side_effect = Exception("Connection failed")
|
|
250
|
+
|
|
251
|
+
result = await analyze_mcp_environment("test:latest")
|
|
252
|
+
|
|
253
|
+
assert result["success"] is False
|
|
254
|
+
assert result["toolCount"] == 0
|
|
255
|
+
assert "error" in result
|
|
256
|
+
assert "Connection failed" in result["error"]
|
|
257
|
+
|
|
258
|
+
@mock.patch("hud.cli.build.MCPClient")
|
|
259
|
+
async def test_analyze_verbose_mode(self, mock_client_class):
|
|
260
|
+
"""Test analysis in verbose mode."""
|
|
261
|
+
mock_client = mock.AsyncMock()
|
|
262
|
+
mock_client_class.return_value = mock_client
|
|
263
|
+
mock_client.list_tools.return_value = []
|
|
264
|
+
|
|
265
|
+
# Just test that it runs without error in verbose mode
|
|
266
|
+
result = await analyze_mcp_environment("test:latest", verbose=True)
|
|
267
|
+
|
|
268
|
+
assert result["success"] is True
|
|
269
|
+
assert "initializeMs" in result
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class TestBuildDockerImage:
|
|
273
|
+
"""Test building Docker images."""
|
|
274
|
+
|
|
275
|
+
@mock.patch("subprocess.Popen")
|
|
276
|
+
def test_build_success(self, mock_popen, tmp_path):
|
|
277
|
+
"""Test successful Docker build."""
|
|
278
|
+
# Create Dockerfile
|
|
279
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
280
|
+
dockerfile.write_text("FROM python:3.11")
|
|
281
|
+
|
|
282
|
+
# 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
|
|
288
|
+
|
|
289
|
+
result = build_docker_image(tmp_path, "test:latest")
|
|
290
|
+
assert result is True
|
|
291
|
+
|
|
292
|
+
@mock.patch("subprocess.Popen")
|
|
293
|
+
def test_build_failure(self, mock_popen, tmp_path):
|
|
294
|
+
"""Test failed Docker build."""
|
|
295
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
296
|
+
dockerfile.write_text("FROM python:3.11")
|
|
297
|
+
|
|
298
|
+
# 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
|
|
304
|
+
|
|
305
|
+
result = build_docker_image(tmp_path, "test:latest")
|
|
306
|
+
assert result is False
|
|
307
|
+
|
|
308
|
+
def test_build_no_dockerfile(self, tmp_path):
|
|
309
|
+
"""Test build when Dockerfile missing."""
|
|
310
|
+
result = build_docker_image(tmp_path, "test:latest")
|
|
311
|
+
assert result is False
|
|
312
|
+
|
|
313
|
+
@mock.patch("subprocess.Popen")
|
|
314
|
+
def test_build_with_no_cache(self, mock_popen, tmp_path):
|
|
315
|
+
"""Test build with --no-cache flag."""
|
|
316
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
317
|
+
dockerfile.write_text("FROM python:3.11")
|
|
318
|
+
|
|
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
|
|
324
|
+
|
|
325
|
+
build_docker_image(tmp_path, "test:latest", no_cache=True)
|
|
326
|
+
|
|
327
|
+
# Check that --no-cache was included
|
|
328
|
+
call_args = mock_popen.call_args[0][0]
|
|
329
|
+
assert "--no-cache" in call_args
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TestBuildEnvironment:
|
|
333
|
+
"""Test the main build_environment function."""
|
|
334
|
+
|
|
335
|
+
@mock.patch("hud.cli.build.build_docker_image")
|
|
336
|
+
@mock.patch("hud.cli.build.analyze_mcp_environment")
|
|
337
|
+
@mock.patch("hud.cli.build.save_to_registry")
|
|
338
|
+
@mock.patch("hud.cli.build.get_docker_image_id")
|
|
339
|
+
@mock.patch("subprocess.Popen")
|
|
340
|
+
@mock.patch("subprocess.run")
|
|
341
|
+
def test_build_environment_success(
|
|
342
|
+
self,
|
|
343
|
+
mock_run,
|
|
344
|
+
mock_popen,
|
|
345
|
+
mock_get_id,
|
|
346
|
+
mock_save_registry,
|
|
347
|
+
mock_analyze,
|
|
348
|
+
mock_build_docker,
|
|
349
|
+
tmp_path,
|
|
350
|
+
):
|
|
351
|
+
"""Test successful environment build."""
|
|
352
|
+
# Setup directory structure
|
|
353
|
+
env_dir = tmp_path / "test-env"
|
|
354
|
+
env_dir.mkdir()
|
|
355
|
+
|
|
356
|
+
# Create pyproject.toml
|
|
357
|
+
pyproject = env_dir / "pyproject.toml"
|
|
358
|
+
pyproject.write_text("""
|
|
359
|
+
[tool.hud]
|
|
360
|
+
image = "test/env:dev"
|
|
361
|
+
""")
|
|
362
|
+
|
|
363
|
+
# Create Dockerfile
|
|
364
|
+
dockerfile = env_dir / "Dockerfile"
|
|
365
|
+
dockerfile.write_text("""
|
|
366
|
+
FROM python:3.11
|
|
367
|
+
ENV API_KEY
|
|
368
|
+
""")
|
|
369
|
+
|
|
370
|
+
# Mock functions
|
|
371
|
+
mock_build_docker.return_value = True
|
|
372
|
+
mock_analyze.return_value = {
|
|
373
|
+
"success": True,
|
|
374
|
+
"toolCount": 2,
|
|
375
|
+
"initializeMs": 1500,
|
|
376
|
+
"tools": [
|
|
377
|
+
{"name": "tool1", "description": "Tool 1"},
|
|
378
|
+
{"name": "tool2", "description": "Tool 2"},
|
|
379
|
+
],
|
|
380
|
+
}
|
|
381
|
+
mock_get_id.return_value = "sha256:abc123"
|
|
382
|
+
|
|
383
|
+
# 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
|
|
392
|
+
|
|
393
|
+
# Run build
|
|
394
|
+
build_environment(str(env_dir), "test/env:latest")
|
|
395
|
+
|
|
396
|
+
# Check lock file was created
|
|
397
|
+
lock_file = env_dir / "hud.lock.yaml"
|
|
398
|
+
assert lock_file.exists()
|
|
399
|
+
|
|
400
|
+
# Verify lock file content
|
|
401
|
+
with open(lock_file) as f:
|
|
402
|
+
lock_data = yaml.safe_load(f)
|
|
403
|
+
|
|
404
|
+
assert lock_data["image"] == "test/env:latest@sha256:abc123"
|
|
405
|
+
assert lock_data["build"]["version"] == "0.1.0"
|
|
406
|
+
assert lock_data["environment"]["toolCount"] == 2
|
|
407
|
+
assert len(lock_data["tools"]) == 2
|
|
408
|
+
|
|
409
|
+
def test_build_environment_no_directory(self):
|
|
410
|
+
"""Test build when directory doesn't exist."""
|
|
411
|
+
with pytest.raises(typer.Exit):
|
|
412
|
+
build_environment("/nonexistent/path")
|
|
413
|
+
|
|
414
|
+
def test_build_environment_no_pyproject(self, tmp_path):
|
|
415
|
+
"""Test build when pyproject.toml missing."""
|
|
416
|
+
with pytest.raises(typer.Exit):
|
|
417
|
+
build_environment(str(tmp_path))
|
|
418
|
+
|
|
419
|
+
@mock.patch("hud.cli.build.build_docker_image")
|
|
420
|
+
def test_build_environment_docker_failure(self, mock_build, tmp_path):
|
|
421
|
+
"""Test when Docker build fails."""
|
|
422
|
+
env_dir = tmp_path / "test-env"
|
|
423
|
+
env_dir.mkdir()
|
|
424
|
+
(env_dir / "pyproject.toml").write_text("[tool.hud]")
|
|
425
|
+
(env_dir / "Dockerfile").write_text("FROM python:3.11")
|
|
426
|
+
|
|
427
|
+
mock_build.return_value = False
|
|
428
|
+
|
|
429
|
+
with pytest.raises(typer.Exit):
|
|
430
|
+
build_environment(str(env_dir))
|
|
431
|
+
|
|
432
|
+
@mock.patch("hud.cli.build.build_docker_image")
|
|
433
|
+
@mock.patch("hud.cli.build.analyze_mcp_environment")
|
|
434
|
+
def test_build_environment_analysis_failure(self, mock_analyze, mock_build, tmp_path):
|
|
435
|
+
"""Test when MCP analysis fails."""
|
|
436
|
+
env_dir = tmp_path / "test-env"
|
|
437
|
+
env_dir.mkdir()
|
|
438
|
+
(env_dir / "pyproject.toml").write_text("[tool.hud]")
|
|
439
|
+
(env_dir / "Dockerfile").write_text("FROM python:3.11")
|
|
440
|
+
|
|
441
|
+
mock_build.return_value = True
|
|
442
|
+
mock_analyze.return_value = {
|
|
443
|
+
"success": False,
|
|
444
|
+
"error": "Connection failed",
|
|
445
|
+
"toolCount": 0,
|
|
446
|
+
"tools": [],
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
with pytest.raises(typer.Exit):
|
|
450
|
+
build_environment(str(env_dir))
|