hud-python 0.4.52__py3-none-any.whl → 0.4.53__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 +9 -2
- hud/agents/openai_chat_generic.py +15 -3
- hud/agents/tests/test_base.py +15 -0
- hud/agents/tests/test_base_runtime.py +164 -0
- hud/cli/__init__.py +6 -3
- hud/cli/build.py +35 -27
- hud/cli/dev.py +11 -29
- hud/cli/eval.py +61 -61
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +24 -2
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +134 -0
- hud/cli/tests/test_mcp_server.py +8 -7
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/utils/docker.py +120 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +2 -2
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_runner.py +106 -0
- hud/datasets/tests/test_utils.py +228 -0
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_instrumentation.py +207 -0
- hud/server/tests/test_server_extra.py +2 -0
- hud/shared/exceptions.py +35 -4
- hud/shared/hints.py +25 -0
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +31 -23
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/tests/test_async_context.py +242 -0
- hud/telemetry/tests/test_instrument.py +414 -0
- hud/telemetry/tests/test_job.py +609 -0
- hud/telemetry/tests/test_trace.py +183 -5
- hud/tools/computer/settings.py +2 -2
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/types.py +7 -1
- hud/utils/agent_factories.py +1 -3
- hud/utils/mcp.py +1 -1
- hud/utils/tests/test_agent_factories.py +60 -0
- hud/utils/tests/test_mcp.py +4 -6
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tasks.py +187 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/METADATA +47 -48
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/RECORD +69 -31
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/WHEEL +0 -0
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.52.dist-info → hud_python-0.4.53.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from hud.cli.analyze import (
|
|
9
|
+
analyze_environment,
|
|
10
|
+
analyze_environment_from_config,
|
|
11
|
+
analyze_environment_from_mcp_config,
|
|
12
|
+
display_interactive,
|
|
13
|
+
display_markdown,
|
|
14
|
+
parse_docker_command,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Mark entire module as asyncio to ensure async tests run with pytest-asyncio
|
|
22
|
+
pytestmark = pytest.mark.asyncio
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parse_docker_command():
|
|
26
|
+
cmd = ["docker", "run", "--rm", "-i", "img"]
|
|
27
|
+
cfg = parse_docker_command(cmd)
|
|
28
|
+
assert cfg == {"local": {"command": "docker", "args": ["run", "--rm", "-i", "img"]}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.asyncio
|
|
32
|
+
@patch("hud.cli.analyze.MCPClient")
|
|
33
|
+
@patch("hud.cli.analyze.console")
|
|
34
|
+
async def test_analyze_environment_success_json(mock_console, MockClient):
|
|
35
|
+
client = AsyncMock()
|
|
36
|
+
client.initialize.return_value = None
|
|
37
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
38
|
+
client.shutdown.return_value = None
|
|
39
|
+
MockClient.return_value = client
|
|
40
|
+
|
|
41
|
+
await analyze_environment(["docker", "run", "img"], output_format="json", verbose=False)
|
|
42
|
+
assert client.initialize.awaited
|
|
43
|
+
assert client.analyze_environment.awaited
|
|
44
|
+
assert client.shutdown.awaited
|
|
45
|
+
assert mock_console.print_json.called
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.asyncio
|
|
49
|
+
@patch("hud.cli.analyze.MCPClient")
|
|
50
|
+
@patch("hud.cli.analyze.console")
|
|
51
|
+
async def test_analyze_environment_failure(mock_console, MockClient):
|
|
52
|
+
client = AsyncMock()
|
|
53
|
+
client.initialize.side_effect = RuntimeError("boom")
|
|
54
|
+
client.shutdown.return_value = None
|
|
55
|
+
MockClient.return_value = client
|
|
56
|
+
|
|
57
|
+
# Should swallow exception and return without raising
|
|
58
|
+
await analyze_environment(["docker", "run", "img"], output_format="json", verbose=True)
|
|
59
|
+
assert client.shutdown.awaited
|
|
60
|
+
assert mock_console.print_json.called is False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_display_interactive_metadata_only(monkeypatch):
|
|
64
|
+
import hud.cli.analyze as mod
|
|
65
|
+
|
|
66
|
+
monkeypatch.setattr(mod, "console", MagicMock(), raising=False)
|
|
67
|
+
monkeypatch.setattr(mod, "hud_console", MagicMock(), raising=False)
|
|
68
|
+
|
|
69
|
+
analysis = {
|
|
70
|
+
"image": "img:latest",
|
|
71
|
+
"status": "cached",
|
|
72
|
+
"tool_count": 2,
|
|
73
|
+
"tools": [
|
|
74
|
+
{"name": "t1", "description": "d1", "inputSchema": {"type": "object"}},
|
|
75
|
+
{"name": "t2", "description": "d2"},
|
|
76
|
+
],
|
|
77
|
+
"resources": [],
|
|
78
|
+
}
|
|
79
|
+
display_interactive(analysis)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_display_markdown_both_paths(capsys):
|
|
83
|
+
# metadata-only
|
|
84
|
+
md_only = {"image": "img:latest", "tool_count": 0, "tools": [], "resources": []}
|
|
85
|
+
display_markdown(md_only)
|
|
86
|
+
|
|
87
|
+
# live metadata
|
|
88
|
+
live = {"metadata": {"servers": ["s1"], "initialized": True}, "tools": [], "resources": []}
|
|
89
|
+
display_markdown(live)
|
|
90
|
+
|
|
91
|
+
# Check that output was generated
|
|
92
|
+
captured = capsys.readouterr()
|
|
93
|
+
assert "MCP Environment Analysis" in captured.out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@patch("hud.cli.analyze.MCPClient")
|
|
97
|
+
async def test_analyze_environment_from_config(MockClient, tmp_path: Path):
|
|
98
|
+
client = AsyncMock()
|
|
99
|
+
client.initialize.return_value = None
|
|
100
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
101
|
+
client.shutdown.return_value = None
|
|
102
|
+
MockClient.return_value = client
|
|
103
|
+
|
|
104
|
+
cfg = tmp_path / "mcp.json"
|
|
105
|
+
cfg.write_text('{"local": {"command": "docker", "args": ["run", "img"]}}')
|
|
106
|
+
await analyze_environment_from_config(cfg, output_format="json", verbose=False)
|
|
107
|
+
assert client.initialize.awaited and client.shutdown.awaited
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@patch("hud.cli.analyze.MCPClient")
|
|
111
|
+
async def test_analyze_environment_from_mcp_config(MockClient):
|
|
112
|
+
client = AsyncMock()
|
|
113
|
+
client.initialize.return_value = None
|
|
114
|
+
client.analyze_environment.return_value = {"tools": [], "resources": []}
|
|
115
|
+
client.shutdown.return_value = None
|
|
116
|
+
MockClient.return_value = client
|
|
117
|
+
|
|
118
|
+
mcp_config = {"local": {"command": "docker", "args": ["run", "img"]}}
|
|
119
|
+
await analyze_environment_from_mcp_config(mcp_config, output_format="json", verbose=False)
|
|
120
|
+
assert client.initialize.awaited and client.shutdown.awaited
|
hud/cli/tests/test_build.py
CHANGED
|
@@ -219,6 +219,17 @@ class TestAnalyzeMcpEnvironment:
|
|
|
219
219
|
mock_tool.description = "Test tool"
|
|
220
220
|
mock_tool.inputSchema = {"type": "object"}
|
|
221
221
|
|
|
222
|
+
# Prefer analyze_environment path (aligns with analyze CLI tests)
|
|
223
|
+
mock_client.analyze_environment = mock.AsyncMock(
|
|
224
|
+
return_value={
|
|
225
|
+
"metadata": {"servers": ["local"], "initialized": True},
|
|
226
|
+
"tools": [{"name": "test_tool", "description": "Test tool"}],
|
|
227
|
+
"hub_tools": {},
|
|
228
|
+
"resources": [],
|
|
229
|
+
"telemetry": {},
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
# Fallback still defined for completeness
|
|
222
233
|
mock_client.list_tools.return_value = [mock_tool]
|
|
223
234
|
|
|
224
235
|
result = await analyze_mcp_environment("test:latest")
|
|
@@ -237,7 +248,9 @@ class TestAnalyzeMcpEnvironment:
|
|
|
237
248
|
mock_client_class.return_value = mock_client
|
|
238
249
|
mock_client.initialize.side_effect = ConnectionError("Connection failed")
|
|
239
250
|
|
|
240
|
-
|
|
251
|
+
from hud.shared.exceptions import HudException
|
|
252
|
+
|
|
253
|
+
with pytest.raises(HudException, match="Connection failed"):
|
|
241
254
|
await analyze_mcp_environment("test:latest")
|
|
242
255
|
|
|
243
256
|
@mock.patch("hud.cli.build.MCPClient")
|
|
@@ -245,6 +258,15 @@ class TestAnalyzeMcpEnvironment:
|
|
|
245
258
|
"""Test analysis in verbose mode."""
|
|
246
259
|
mock_client = mock.AsyncMock()
|
|
247
260
|
mock_client_class.return_value = mock_client
|
|
261
|
+
mock_client.analyze_environment = mock.AsyncMock(
|
|
262
|
+
return_value={
|
|
263
|
+
"metadata": {"servers": ["local"], "initialized": True},
|
|
264
|
+
"tools": [],
|
|
265
|
+
"hub_tools": {},
|
|
266
|
+
"resources": [],
|
|
267
|
+
"telemetry": {},
|
|
268
|
+
}
|
|
269
|
+
)
|
|
248
270
|
mock_client.list_tools.return_value = []
|
|
249
271
|
|
|
250
272
|
# Just test that it runs without error in verbose mode
|
|
@@ -363,7 +385,7 @@ ENV API_KEY
|
|
|
363
385
|
mock_run.return_value = mock_result
|
|
364
386
|
|
|
365
387
|
# Run build
|
|
366
|
-
build_environment(str(env_dir), "test
|
|
388
|
+
build_environment(str(env_dir), "test-env:latest")
|
|
367
389
|
|
|
368
390
|
# Check lock file was created
|
|
369
391
|
lock_file = env_dir / "hud.lock.yaml"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from hud.cli.build import build_environment
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@patch("hud.cli.build.compute_source_hash", return_value="deadbeef")
|
|
16
|
+
@patch(
|
|
17
|
+
"hud.cli.build.analyze_mcp_environment",
|
|
18
|
+
return_value={"initializeMs": 10, "toolCount": 0, "tools": []},
|
|
19
|
+
)
|
|
20
|
+
@patch("hud.cli.build.build_docker_image", return_value=True)
|
|
21
|
+
def test_build_label_rebuild_failure(_bd, _an, _hash, tmp_path: Path, monkeypatch):
|
|
22
|
+
# Minimal environment dir
|
|
23
|
+
env = tmp_path / "env"
|
|
24
|
+
env.mkdir()
|
|
25
|
+
(env / "Dockerfile").write_text("FROM python:3.11")
|
|
26
|
+
|
|
27
|
+
# Ensure subprocess.run returns non-zero for the second build (label build)
|
|
28
|
+
import types
|
|
29
|
+
|
|
30
|
+
def run_side_effect(cmd, *a, **k):
|
|
31
|
+
# Return 0 for first docker build, 1 for label build
|
|
32
|
+
if isinstance(cmd, list) and cmd[:2] == ["docker", "build"] and "--label" in cmd:
|
|
33
|
+
return types.SimpleNamespace(returncode=1, stderr="boom")
|
|
34
|
+
return types.SimpleNamespace(returncode=0, stdout="")
|
|
35
|
+
|
|
36
|
+
monkeypatch.setenv("FASTMCP_DISABLE_BANNER", "1")
|
|
37
|
+
with (
|
|
38
|
+
patch("hud.cli.build.subprocess.run", side_effect=run_side_effect),
|
|
39
|
+
pytest.raises(typer.Exit),
|
|
40
|
+
):
|
|
41
|
+
build_environment(str(env), verbose=False)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest import mock
|
|
5
|
+
|
|
6
|
+
from hud.cli.build import (
|
|
7
|
+
extract_env_vars_from_dockerfile,
|
|
8
|
+
get_docker_image_digest,
|
|
9
|
+
get_docker_image_id,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_extract_env_vars_from_dockerfile_complex(tmp_path: Path):
|
|
17
|
+
dockerfile = tmp_path / "Dockerfile"
|
|
18
|
+
dockerfile.write_text(
|
|
19
|
+
"""
|
|
20
|
+
FROM python:3.11
|
|
21
|
+
ARG BUILD_TOKEN
|
|
22
|
+
ARG DEFAULTED=1
|
|
23
|
+
ENV RUNTIME_KEY
|
|
24
|
+
ENV FROM_ARG=$BUILD_TOKEN
|
|
25
|
+
ENV WITH_DEFAULT=val
|
|
26
|
+
"""
|
|
27
|
+
)
|
|
28
|
+
required, optional = extract_env_vars_from_dockerfile(dockerfile)
|
|
29
|
+
# BUILD_TOKEN required (ARG without default)
|
|
30
|
+
assert "BUILD_TOKEN" in required
|
|
31
|
+
# RUNTIME_KEY required (ENV without value)
|
|
32
|
+
assert "RUNTIME_KEY" in required
|
|
33
|
+
# FROM_ARG references BUILD_TOKEN -> required
|
|
34
|
+
assert "FROM_ARG" in required
|
|
35
|
+
# DEFAULTED and WITH_DEFAULT should not be marked required by default
|
|
36
|
+
assert "DEFAULTED" not in required
|
|
37
|
+
assert "WITH_DEFAULT" not in required
|
|
38
|
+
assert optional == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mock.patch("subprocess.run")
|
|
42
|
+
def test_get_docker_image_digest_none(mock_run):
|
|
43
|
+
mock_run.return_value = mock.Mock(stdout="[]", returncode=0)
|
|
44
|
+
assert get_docker_image_digest("img") is None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mock.patch("subprocess.run")
|
|
48
|
+
def test_get_docker_image_id_ok(mock_run):
|
|
49
|
+
mock_run.return_value = mock.Mock(stdout="sha256:abc", returncode=0)
|
|
50
|
+
assert get_docker_image_id("img") == "sha256:abc"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import hud.cli as cli
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_version_does_not_crash():
|
|
9
|
+
# Just ensure it runs without raising
|
|
10
|
+
cli.version()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@patch("hud.cli.list_module.list_command")
|
|
14
|
+
def test_list_environments_wrapper(mock_list):
|
|
15
|
+
cli.list_environments(filter_name=None, json_output=False, show_all=False, verbose=False)
|
|
16
|
+
assert mock_list.called
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@patch("hud.cli.clone_repository", return_value=(True, "/tmp/repo"))
|
|
20
|
+
@patch("hud.cli.get_clone_message", return_value={})
|
|
21
|
+
@patch("hud.cli.print_tutorial")
|
|
22
|
+
def test_clone_wrapper(mock_tutorial, _msg, _clone):
|
|
23
|
+
cli.clone("https://example.com/repo.git")
|
|
24
|
+
assert mock_tutorial.called
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@patch("hud.cli.remove_command")
|
|
28
|
+
def test_remove_wrapper(mock_remove):
|
|
29
|
+
cli.remove(target="all", yes=True, verbose=False)
|
|
30
|
+
assert mock_remove.called
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import hud.cli as cli
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@patch("hud.cli.utils.metadata.analyze_from_metadata", new_callable=AsyncMock)
|
|
15
|
+
@patch("asyncio.run")
|
|
16
|
+
def test_analyze_params_metadata(mock_run, mock_analyze):
|
|
17
|
+
# image only -> metadata path
|
|
18
|
+
cli.analyze(params=["img:latest"], output_format="json", verbose=False)
|
|
19
|
+
assert mock_run.called
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@patch("hud.cli.analyze.analyze_environment", new_callable=AsyncMock)
|
|
23
|
+
@patch("hud.cli.utils.docker.build_run_command")
|
|
24
|
+
@patch("asyncio.run")
|
|
25
|
+
def test_analyze_params_live(mock_run, mock_build_cmd, mock_analyze_env):
|
|
26
|
+
mock_build_cmd.return_value = ["docker", "run", "img", "-e", "K=V"]
|
|
27
|
+
# docker args trigger live path
|
|
28
|
+
cli.analyze(params=["img:latest", "-e", "K=V"], output_format="json", verbose=True)
|
|
29
|
+
assert mock_run.called
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_analyze_no_params_errors():
|
|
33
|
+
import typer
|
|
34
|
+
|
|
35
|
+
# When no params provided, analyze prints help and exits(1)
|
|
36
|
+
with pytest.raises(typer.Exit):
|
|
37
|
+
cli.analyze(params=None, config=None, cursor=None, output_format="json", verbose=False) # type: ignore
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@patch("hud.cli.analyze.analyze_environment_from_config", new_callable=AsyncMock)
|
|
41
|
+
@patch("asyncio.run")
|
|
42
|
+
def test_analyze_from_config(mock_run, mock_func, tmp_path: Path):
|
|
43
|
+
cfg = tmp_path / "cfg.json"
|
|
44
|
+
cfg.write_text("{}")
|
|
45
|
+
cli.analyze(params=None, config=cfg, cursor=None, output_format="json", verbose=False) # type: ignore
|
|
46
|
+
assert mock_run.called
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@patch("hud.cli.parse_cursor_config")
|
|
50
|
+
@patch("hud.cli.analyze.analyze_environment_from_mcp_config", new_callable=AsyncMock)
|
|
51
|
+
@patch("asyncio.run")
|
|
52
|
+
def test_analyze_from_cursor(mock_run, mock_analyze, mock_parse):
|
|
53
|
+
mock_parse.return_value = (["cmd", "arg"], None)
|
|
54
|
+
cli.analyze(params=None, config=None, cursor="server", output_format="json", verbose=False) # type: ignore
|
|
55
|
+
assert mock_run.called
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@patch("hud.cli.build_command")
|
|
59
|
+
def test_build_env_var_parsing(mock_build):
|
|
60
|
+
cli.build(
|
|
61
|
+
params=[".", "-e", "A=B", "--env=C=D", "--env", "E=F"],
|
|
62
|
+
tag=None,
|
|
63
|
+
no_cache=False,
|
|
64
|
+
verbose=False,
|
|
65
|
+
platform=None,
|
|
66
|
+
)
|
|
67
|
+
assert mock_build.called
|
|
68
|
+
# args: directory, tag, no_cache, verbose, env_vars, platform
|
|
69
|
+
env_vars = mock_build.call_args[0][4]
|
|
70
|
+
assert env_vars == {"A": "B", "C": "D", "E": "F"}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@patch("hud.cli.utils.runner.run_mcp_server")
|
|
74
|
+
def test_run_local_calls_runner(mock_runner):
|
|
75
|
+
cli.run(
|
|
76
|
+
params=["img:latest"],
|
|
77
|
+
local=True,
|
|
78
|
+
transport="stdio",
|
|
79
|
+
port=1234,
|
|
80
|
+
url=None, # type: ignore
|
|
81
|
+
api_key=None,
|
|
82
|
+
run_id=None,
|
|
83
|
+
verbose=False,
|
|
84
|
+
)
|
|
85
|
+
assert mock_runner.called
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@patch("hud.cli.utils.remote_runner.run_remote_server")
|
|
89
|
+
def test_run_remote_calls_remote(mock_remote):
|
|
90
|
+
cli.run(
|
|
91
|
+
params=["img:latest"],
|
|
92
|
+
local=False,
|
|
93
|
+
transport="http",
|
|
94
|
+
port=8765,
|
|
95
|
+
url="https://x",
|
|
96
|
+
api_key=None,
|
|
97
|
+
run_id=None,
|
|
98
|
+
verbose=True,
|
|
99
|
+
)
|
|
100
|
+
assert mock_remote.called
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_run_no_params_errors():
|
|
104
|
+
import typer
|
|
105
|
+
|
|
106
|
+
with pytest.raises(typer.Exit):
|
|
107
|
+
cli.run(params=None) # type: ignore
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@patch("hud.cli.run_mcp_dev_server")
|
|
111
|
+
def test_dev_calls_runner(mock_dev):
|
|
112
|
+
cli.dev(
|
|
113
|
+
params=["server.main"],
|
|
114
|
+
docker=False,
|
|
115
|
+
stdio=False,
|
|
116
|
+
port=9000,
|
|
117
|
+
verbose=False,
|
|
118
|
+
inspector=False,
|
|
119
|
+
interactive=False,
|
|
120
|
+
watch=None, # type: ignore
|
|
121
|
+
)
|
|
122
|
+
assert mock_dev.called
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@patch("hud.cli.pull_command")
|
|
126
|
+
def test_pull_command_wrapper(mock_pull):
|
|
127
|
+
cli.pull(target="org/name:tag", lock_file=None, yes=True, verify_only=True, verbose=False)
|
|
128
|
+
assert mock_pull.called
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@patch("hud.cli.push_command")
|
|
132
|
+
def test_push_command_wrapper(mock_push, tmp_path: Path):
|
|
133
|
+
cli.push(directory=str(tmp_path), image=None, tag=None, sign=False, yes=True, verbose=True)
|
|
134
|
+
assert mock_push.called
|
hud/cli/tests/test_mcp_server.py
CHANGED
|
@@ -15,22 +15,23 @@ class TestRunMCPDevServer:
|
|
|
15
15
|
"""Test the main server runner."""
|
|
16
16
|
|
|
17
17
|
def test_run_dev_server_image_not_found(self) -> None:
|
|
18
|
-
"""
|
|
19
|
-
import
|
|
18
|
+
"""When using Docker mode without a lock file, exits with typer.Exit(1)."""
|
|
19
|
+
import typer
|
|
20
20
|
|
|
21
21
|
with (
|
|
22
|
-
patch("hud.cli.
|
|
23
|
-
patch("
|
|
24
|
-
|
|
22
|
+
patch("hud.cli.dev.should_use_docker_mode", return_value=True),
|
|
23
|
+
patch("hud.cli.dev.Path.cwd"),
|
|
24
|
+
patch("hud.cli.dev.hud_console"),
|
|
25
|
+
pytest.raises(typer.Exit),
|
|
25
26
|
):
|
|
26
27
|
run_mcp_dev_server(
|
|
27
|
-
module=
|
|
28
|
+
module=None,
|
|
28
29
|
stdio=False,
|
|
29
30
|
port=8765,
|
|
30
31
|
verbose=False,
|
|
31
32
|
inspector=False,
|
|
32
33
|
interactive=False,
|
|
33
34
|
watch=[],
|
|
34
|
-
docker=
|
|
35
|
+
docker=True,
|
|
35
36
|
docker_args=[],
|
|
36
37
|
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from hud.cli.push import push_environment
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@patch("hud.cli.push.get_docker_username", return_value="tester")
|
|
14
|
+
@patch(
|
|
15
|
+
"hud.cli.push.get_docker_image_labels",
|
|
16
|
+
return_value={"org.hud.manifest.head": "abc", "org.hud.version": "0.1.0"},
|
|
17
|
+
)
|
|
18
|
+
@patch("hud.cli.push.requests.post")
|
|
19
|
+
@patch("hud.cli.push.subprocess.Popen")
|
|
20
|
+
@patch("hud.cli.push.subprocess.run")
|
|
21
|
+
def test_push_happy_path(
|
|
22
|
+
mock_run, mock_popen, mock_post, _labels, _user, tmp_path: Path, monkeypatch
|
|
23
|
+
):
|
|
24
|
+
# Prepare minimal environment with lock file
|
|
25
|
+
env_dir = tmp_path
|
|
26
|
+
(env_dir / "hud.lock.yaml").write_text(
|
|
27
|
+
"images:\n local: org/env:latest\nbuild:\n version: 0.1.0\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Provide API key via main settings module
|
|
31
|
+
monkeypatch.setattr("hud.settings.settings.api_key", "sk-test", raising=False)
|
|
32
|
+
|
|
33
|
+
# ensure_built noop - patch from the right module
|
|
34
|
+
monkeypatch.setattr("hud.cli.utils.env_check.ensure_built", lambda *_a, **_k: {})
|
|
35
|
+
|
|
36
|
+
# Mock subprocess.run behavior depending on command
|
|
37
|
+
def run_side_effect(args, *a, **k):
|
|
38
|
+
cmd = list(args)
|
|
39
|
+
# docker inspect checks
|
|
40
|
+
if cmd[:2] == ["docker", "inspect"]:
|
|
41
|
+
# For label digest query at end
|
|
42
|
+
if "--format" in cmd and "{{index .RepoDigests 0}}" in cmd:
|
|
43
|
+
return SimpleNamespace(returncode=0, stdout="org/env@sha256:deadbeef")
|
|
44
|
+
# Existence checks succeed
|
|
45
|
+
return SimpleNamespace(returncode=0, stdout="")
|
|
46
|
+
# docker tag success
|
|
47
|
+
if cmd[:2] == ["docker", "tag"]:
|
|
48
|
+
return SimpleNamespace(returncode=0, stdout="")
|
|
49
|
+
return SimpleNamespace(returncode=0, stdout="")
|
|
50
|
+
|
|
51
|
+
mock_run.side_effect = run_side_effect
|
|
52
|
+
|
|
53
|
+
# Mock Popen push pipeline
|
|
54
|
+
class _Proc:
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self.stdout = ["digest: sha256:deadbeef\n", "pushed\n"]
|
|
57
|
+
self.returncode = 0
|
|
58
|
+
|
|
59
|
+
def wait(self):
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
mock_popen.return_value = _Proc()
|
|
63
|
+
|
|
64
|
+
# Mock registry POST success
|
|
65
|
+
mock_post.return_value = SimpleNamespace(status_code=201, json=lambda: {"ok": True}, text="")
|
|
66
|
+
|
|
67
|
+
# Execute
|
|
68
|
+
push_environment(
|
|
69
|
+
directory=str(env_dir), image=None, tag=None, sign=False, yes=True, verbose=False
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Lock file updated with pushed entry
|
|
73
|
+
data = (env_dir / "hud.lock.yaml").read_text()
|
|
74
|
+
assert "pushed:" in data
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from hud.cli.push import push_environment
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@patch("hud.cli.push.ensure_built")
|
|
16
|
+
@patch("hud.cli.push.HUDConsole")
|
|
17
|
+
@patch("hud.cli.push.subprocess.run")
|
|
18
|
+
def test_push_environment_missing_lock_raises(mock_run, mock_console, _ensure, tmp_path: Path):
|
|
19
|
+
# No hud.lock.yaml → Exit(1)
|
|
20
|
+
with pytest.raises(typer.Exit):
|
|
21
|
+
push_environment(
|
|
22
|
+
directory=str(tmp_path), image=None, tag=None, sign=False, yes=True, verbose=False
|
|
23
|
+
)
|