hud-python 0.4.45__py3-none-any.whl → 0.5.1__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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +11 -5
- hud/agents/base.py +220 -500
- hud/agents/claude.py +200 -240
- hud/agents/gemini.py +275 -0
- hud/agents/gemini_cua.py +335 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +41 -36
- hud/agents/openai.py +291 -292
- hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
- hud/agents/operator.py +211 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +379 -210
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +376 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/cli/__init__.py +461 -545
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +664 -110
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +882 -734
- hud/cli/eval.py +782 -668
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/push.py +29 -11
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +108 -6
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +69 -0
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -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_git.py +142 -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 +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +40 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +327 -0
- hud/datasets/runner.py +192 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +50 -0
- hud/environment/connection.py +206 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +109 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +694 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +112 -0
- hud/environment/scenarios.py +493 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +218 -0
- hud/environment/tests/test_environment.py +161 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +201 -0
- hud/environment/tests/test_scenarios.py +280 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +674 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +185 -0
- hud/eval/manager.py +466 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +340 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +145 -0
- hud/eval/types.py +63 -0
- hud/eval/utils.py +183 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +151 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +158 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +16 -2
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +4 -0
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +167 -57
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +61 -3
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.1.dist-info/METADATA +264 -0
- hud_python-0.5.1.dist-info/RECORD +299 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
# Import the function directly from the __init__ module to avoid namespace conflict with analyze.py
|
|
11
|
+
import hud.cli.__init__ as cli_init
|
|
12
|
+
|
|
13
|
+
analyze_fn = cli_init.analyze
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@patch("hud.cli.utils.metadata.analyze_from_metadata", new_callable=AsyncMock)
|
|
20
|
+
@patch("asyncio.run")
|
|
21
|
+
def test_analyze_params_metadata(mock_run, mock_analyze):
|
|
22
|
+
# image only -> metadata path
|
|
23
|
+
analyze_fn(params=["img:latest"], output_format="json", verbose=False)
|
|
24
|
+
assert mock_run.called
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@patch("hud.cli.analyze.analyze_environment", new_callable=AsyncMock)
|
|
28
|
+
@patch("hud.cli.utils.docker.build_run_command")
|
|
29
|
+
@patch("asyncio.run")
|
|
30
|
+
def test_analyze_params_live(mock_run, mock_build_cmd, mock_analyze_env):
|
|
31
|
+
mock_build_cmd.return_value = ["docker", "run", "img", "-e", "K=V"]
|
|
32
|
+
# docker args trigger live path
|
|
33
|
+
analyze_fn(params=["img:latest", "-e", "K=V"], output_format="json", verbose=True)
|
|
34
|
+
assert mock_run.called
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_analyze_no_params_errors():
|
|
38
|
+
import typer
|
|
39
|
+
|
|
40
|
+
# When no params provided, analyze prints help and exits(1)
|
|
41
|
+
with pytest.raises(typer.Exit):
|
|
42
|
+
analyze_fn(params=None, config=None, cursor=None, output_format="json", verbose=False) # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@patch("hud.cli.analyze.analyze_environment_from_config", new_callable=AsyncMock)
|
|
46
|
+
@patch("asyncio.run")
|
|
47
|
+
def test_analyze_from_config(mock_run, mock_func, tmp_path: Path):
|
|
48
|
+
cfg = tmp_path / "cfg.json"
|
|
49
|
+
cfg.write_text("{}")
|
|
50
|
+
analyze_fn(params=None, config=cfg, cursor=None, output_format="json", verbose=False) # type: ignore
|
|
51
|
+
assert mock_run.called
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@patch("hud.cli.console")
|
|
55
|
+
@patch("hud.cli.__init__.parse_cursor_config")
|
|
56
|
+
@patch("hud.cli.analyze.analyze_environment_from_mcp_config", new_callable=AsyncMock)
|
|
57
|
+
@patch("asyncio.run")
|
|
58
|
+
def test_analyze_from_cursor(mock_run, mock_analyze, mock_parse, mock_console):
|
|
59
|
+
mock_parse.return_value = (["cmd", "arg"], None)
|
|
60
|
+
analyze_fn(params=None, config=None, cursor="server", output_format="json", verbose=False) # type: ignore
|
|
61
|
+
assert mock_run.called
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@patch("hud.cli.build_command")
|
|
65
|
+
def test_build_env_var_parsing(mock_build):
|
|
66
|
+
cli.build(
|
|
67
|
+
params=[".", "-e", "A=B", "--env=C=D", "--env", "E=F"],
|
|
68
|
+
tag=None,
|
|
69
|
+
no_cache=False,
|
|
70
|
+
verbose=False,
|
|
71
|
+
platform=None,
|
|
72
|
+
)
|
|
73
|
+
assert mock_build.called
|
|
74
|
+
# args: directory, tag, no_cache, verbose, env_vars, platform
|
|
75
|
+
env_vars = mock_build.call_args[0][4]
|
|
76
|
+
assert env_vars == {"A": "B", "C": "D", "E": "F"}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@patch("hud.cli.utils.runner.run_mcp_server")
|
|
80
|
+
def test_run_local_calls_runner(mock_runner):
|
|
81
|
+
cli.run(
|
|
82
|
+
params=["img:latest"],
|
|
83
|
+
local=True,
|
|
84
|
+
transport="stdio",
|
|
85
|
+
port=1234,
|
|
86
|
+
url=None, # type: ignore
|
|
87
|
+
api_key=None,
|
|
88
|
+
run_id=None,
|
|
89
|
+
verbose=False,
|
|
90
|
+
)
|
|
91
|
+
assert mock_runner.called
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@patch("hud.cli.utils.remote_runner.run_remote_server")
|
|
95
|
+
def test_run_remote_calls_remote(mock_remote):
|
|
96
|
+
cli.run(
|
|
97
|
+
params=["img:latest"],
|
|
98
|
+
local=False,
|
|
99
|
+
transport="http",
|
|
100
|
+
port=8765,
|
|
101
|
+
url="https://x",
|
|
102
|
+
api_key=None,
|
|
103
|
+
run_id=None,
|
|
104
|
+
verbose=True,
|
|
105
|
+
)
|
|
106
|
+
assert mock_remote.called
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_run_no_params_errors():
|
|
110
|
+
import typer
|
|
111
|
+
|
|
112
|
+
with pytest.raises(typer.Exit):
|
|
113
|
+
cli.run(params=None) # type: ignore
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@patch("hud.cli.run_mcp_dev_server")
|
|
117
|
+
def test_dev_calls_runner(mock_dev):
|
|
118
|
+
cli.dev(
|
|
119
|
+
params=["server.main"],
|
|
120
|
+
docker=False,
|
|
121
|
+
stdio=False,
|
|
122
|
+
port=9000,
|
|
123
|
+
verbose=False,
|
|
124
|
+
inspector=False,
|
|
125
|
+
interactive=False,
|
|
126
|
+
watch=None, # type: ignore
|
|
127
|
+
)
|
|
128
|
+
assert mock_dev.called
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@patch("hud.cli.pull_command")
|
|
132
|
+
def test_pull_command_wrapper(mock_pull):
|
|
133
|
+
cli.pull(target="org/name:tag", lock_file=None, yes=True, verify_only=True, verbose=False)
|
|
134
|
+
assert mock_pull.called
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@patch("hud.cli.push_command")
|
|
138
|
+
def test_push_command_wrapper(mock_push, tmp_path: Path):
|
|
139
|
+
cli.push(directory=str(tmp_path), image=None, tag=None, sign=False, yes=True, verbose=True)
|
|
140
|
+
assert mock_push.called
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Tests for the convert command."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from hud.cli.flows.tasks import convert_tasks_to_remote
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestConvertCommand:
|
|
14
|
+
"""Test the convert command functionality."""
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def temp_tasks_file(self, tmp_path):
|
|
18
|
+
"""Create a temporary tasks file."""
|
|
19
|
+
tasks = [
|
|
20
|
+
{
|
|
21
|
+
"prompt": "Test task 1",
|
|
22
|
+
"mcp_config": {
|
|
23
|
+
"local": {
|
|
24
|
+
"command": "docker",
|
|
25
|
+
"args": ["run", "--rm", "-i", "test-image:latest"],
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
tasks_file = tmp_path / "tasks.json"
|
|
31
|
+
tasks_file.write_text(json.dumps(tasks))
|
|
32
|
+
return tasks_file
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def mock_env_dir(self, tmp_path):
|
|
36
|
+
"""Create a mock environment directory with lock file."""
|
|
37
|
+
env_dir = tmp_path / "env"
|
|
38
|
+
env_dir.mkdir()
|
|
39
|
+
|
|
40
|
+
# Create lock file
|
|
41
|
+
lock_data = {
|
|
42
|
+
"images": {
|
|
43
|
+
"remote": "registry.hud.ai/test-org/test-env:v1.0.0",
|
|
44
|
+
"local": "test-env:latest",
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
lock_file = env_dir / "hud.lock.yaml"
|
|
48
|
+
import yaml
|
|
49
|
+
|
|
50
|
+
lock_file.write_text(yaml.dump(lock_data))
|
|
51
|
+
|
|
52
|
+
return env_dir
|
|
53
|
+
|
|
54
|
+
@patch("hud.cli.flows.tasks._derive_remote_image")
|
|
55
|
+
@patch("hud.cli.flows.tasks._ensure_pushed")
|
|
56
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
57
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
58
|
+
@patch("hud.settings.settings")
|
|
59
|
+
def test_convert_tasks_basic(
|
|
60
|
+
self,
|
|
61
|
+
mock_settings,
|
|
62
|
+
mock_load_tasks,
|
|
63
|
+
mock_find_env,
|
|
64
|
+
mock_ensure_pushed,
|
|
65
|
+
mock_derive_remote,
|
|
66
|
+
temp_tasks_file,
|
|
67
|
+
mock_env_dir,
|
|
68
|
+
):
|
|
69
|
+
"""Test basic task conversion from local to remote."""
|
|
70
|
+
# Setup mocks
|
|
71
|
+
mock_settings.api_key = "test-api-key"
|
|
72
|
+
mock_settings.hud_mcp_url = "https://mcp.hud.ai/v3/mcp"
|
|
73
|
+
mock_find_env.return_value = mock_env_dir
|
|
74
|
+
|
|
75
|
+
# Mock the push check to return updated lock data
|
|
76
|
+
mock_ensure_pushed.return_value = {
|
|
77
|
+
"images": {
|
|
78
|
+
"remote": "registry.hud.ai/test-org/test-env:v1.0.0",
|
|
79
|
+
"local": "test-env:v1.0.0",
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Mock derive remote image
|
|
84
|
+
mock_derive_remote.return_value = "registry.hud.ai/test-org/test-env:v1.0.0"
|
|
85
|
+
|
|
86
|
+
raw_task = {
|
|
87
|
+
"prompt": "Test task",
|
|
88
|
+
"mcp_config": {
|
|
89
|
+
"local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mock_load_tasks.return_value = [raw_task]
|
|
94
|
+
|
|
95
|
+
# Run conversion
|
|
96
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
97
|
+
|
|
98
|
+
# Check result
|
|
99
|
+
assert result_path.endswith("remote_tasks.json")
|
|
100
|
+
assert Path(result_path).exists()
|
|
101
|
+
|
|
102
|
+
# Verify converted content
|
|
103
|
+
with open(result_path) as f:
|
|
104
|
+
converted_tasks = json.load(f)
|
|
105
|
+
|
|
106
|
+
assert len(converted_tasks) == 1
|
|
107
|
+
assert "hud" in converted_tasks[0]["mcp_config"]
|
|
108
|
+
assert converted_tasks[0]["mcp_config"]["hud"]["url"] == "https://mcp.hud.ai/v3/mcp"
|
|
109
|
+
|
|
110
|
+
@patch("hud.settings.settings")
|
|
111
|
+
def test_convert_missing_api_key(self, mock_settings, temp_tasks_file):
|
|
112
|
+
"""Test that conversion fails without API key."""
|
|
113
|
+
mock_settings.api_key = ""
|
|
114
|
+
|
|
115
|
+
with pytest.raises(typer.Exit):
|
|
116
|
+
convert_tasks_to_remote(str(temp_tasks_file))
|
|
117
|
+
|
|
118
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
119
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
120
|
+
@patch("hud.settings.settings")
|
|
121
|
+
def test_convert_already_remote(
|
|
122
|
+
self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
|
|
123
|
+
):
|
|
124
|
+
"""Test that already remote tasks are not converted again."""
|
|
125
|
+
mock_settings.api_key = "test-api-key"
|
|
126
|
+
mock_find_env.return_value = None # No env dir needed for remote tasks
|
|
127
|
+
|
|
128
|
+
# Create task that's already remote (as raw dict)
|
|
129
|
+
raw_task = {
|
|
130
|
+
"prompt": "Test task",
|
|
131
|
+
"mcp_config": {
|
|
132
|
+
"remote": {
|
|
133
|
+
"url": "https://mcp.hud.ai",
|
|
134
|
+
"headers": {"Mcp-Image": "registry.hud.ai/test/image:v1"},
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
mock_load_tasks.return_value = [raw_task]
|
|
140
|
+
|
|
141
|
+
# Should return original path without modification
|
|
142
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
143
|
+
assert result_path == str(temp_tasks_file)
|
|
144
|
+
|
|
145
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
146
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
147
|
+
@patch("hud.settings.settings")
|
|
148
|
+
def test_convert_no_environment(
|
|
149
|
+
self, mock_settings, mock_load_tasks, mock_find_env, temp_tasks_file
|
|
150
|
+
):
|
|
151
|
+
"""Test that conversion fails when no environment is found."""
|
|
152
|
+
mock_settings.api_key = "test-api-key"
|
|
153
|
+
mock_find_env.return_value = None
|
|
154
|
+
|
|
155
|
+
raw_task = {
|
|
156
|
+
"prompt": "Test task",
|
|
157
|
+
"mcp_config": {
|
|
158
|
+
"local": {"command": "docker", "args": ["run", "--rm", "-i", "test-image:latest"]}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
mock_load_tasks.return_value = [raw_task]
|
|
163
|
+
|
|
164
|
+
with pytest.raises(typer.Exit):
|
|
165
|
+
convert_tasks_to_remote(str(temp_tasks_file))
|
|
166
|
+
|
|
167
|
+
@patch("hud.utils.hud_console.hud_console.confirm")
|
|
168
|
+
@patch("hud.cli.flows.tasks._derive_remote_image")
|
|
169
|
+
@patch("hud.cli.flows.tasks._ensure_pushed")
|
|
170
|
+
@patch("hud.cli.flows.tasks.find_environment_dir")
|
|
171
|
+
@patch("hud.cli.flows.tasks.load_tasks")
|
|
172
|
+
@patch("hud.settings.settings")
|
|
173
|
+
def test_convert_with_env_vars(
|
|
174
|
+
self,
|
|
175
|
+
mock_settings,
|
|
176
|
+
mock_load_tasks,
|
|
177
|
+
mock_find_env,
|
|
178
|
+
mock_ensure_pushed,
|
|
179
|
+
mock_derive_remote,
|
|
180
|
+
mock_confirm,
|
|
181
|
+
temp_tasks_file,
|
|
182
|
+
mock_env_dir,
|
|
183
|
+
):
|
|
184
|
+
"""Test conversion includes environment variables as headers."""
|
|
185
|
+
mock_settings.api_key = "test-api-key"
|
|
186
|
+
mock_settings.hud_mcp_url = "https://mcp.hud.ai/v3/mcp"
|
|
187
|
+
mock_find_env.return_value = mock_env_dir
|
|
188
|
+
mock_confirm.return_value = True # Always confirm in tests
|
|
189
|
+
|
|
190
|
+
# Mock the push check to return updated lock data
|
|
191
|
+
mock_ensure_pushed.return_value = {
|
|
192
|
+
"images": {
|
|
193
|
+
"remote": "registry.hud.ai/test-org/test-env:v1.0.0",
|
|
194
|
+
"local": "test-env:v1.0.0",
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Mock derive remote image
|
|
199
|
+
mock_derive_remote.return_value = "registry.hud.ai/test-org/test-env:v1.0.0"
|
|
200
|
+
|
|
201
|
+
# Add .env file with API keys
|
|
202
|
+
env_file = mock_env_dir / ".env"
|
|
203
|
+
env_file.write_text("OPENAI_API_KEY=sk-test123\nANTHROPIC_API_KEY=sk-ant456")
|
|
204
|
+
|
|
205
|
+
raw_task = {
|
|
206
|
+
"prompt": "Test task",
|
|
207
|
+
"mcp_config": {
|
|
208
|
+
"local": {
|
|
209
|
+
"command": "docker",
|
|
210
|
+
"args": ["run", "--rm", "-i", "-e", "OPENAI_API_KEY", "test-image:latest"],
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
mock_load_tasks.return_value = [raw_task]
|
|
216
|
+
|
|
217
|
+
# Run conversion
|
|
218
|
+
result_path = convert_tasks_to_remote(str(temp_tasks_file))
|
|
219
|
+
|
|
220
|
+
# Verify headers include env vars
|
|
221
|
+
with open(result_path) as f:
|
|
222
|
+
converted_tasks = json.load(f)
|
|
223
|
+
|
|
224
|
+
headers = converted_tasks[0]["mcp_config"]["hud"]["headers"]
|
|
225
|
+
assert "Env-Openai-Api-Key" in headers
|
|
226
|
+
assert headers["Env-Openai-Api-Key"] == "${OPENAI_API_KEY}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestConvertHelperFunctions:
|
|
230
|
+
"""Test helper functions used by convert command."""
|
|
231
|
+
|
|
232
|
+
def test_env_var_to_header_key(self):
|
|
233
|
+
"""Test environment variable name conversion to header format."""
|
|
234
|
+
from hud.cli.flows.tasks import _env_var_to_header_key
|
|
235
|
+
|
|
236
|
+
assert _env_var_to_header_key("OPENAI_API_KEY") == "Env-Openai-Api-Key"
|
|
237
|
+
assert _env_var_to_header_key("ANTHROPIC_API_KEY") == "Env-Anthropic-Api-Key"
|
|
238
|
+
assert _env_var_to_header_key("SIMPLE") == "Env-Simple"
|
|
239
|
+
assert _env_var_to_header_key("MULTIPLE_WORD_VAR") == "Env-Multiple-Word-Var"
|
|
240
|
+
|
|
241
|
+
def test_extract_dotenv_api_key_vars(self):
|
|
242
|
+
"""Test extraction of API-like variables from .env file."""
|
|
243
|
+
# Create test env directory with .env file
|
|
244
|
+
import tempfile
|
|
245
|
+
|
|
246
|
+
from hud.cli.flows.tasks import _extract_dotenv_api_key_vars
|
|
247
|
+
|
|
248
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
249
|
+
env_dir = Path(tmpdir)
|
|
250
|
+
env_file = env_dir / ".env"
|
|
251
|
+
env_file.write_text("""
|
|
252
|
+
# Test .env file
|
|
253
|
+
OPENAI_API_KEY=sk-test123
|
|
254
|
+
ANTHROPIC_API_KEY=sk-ant456
|
|
255
|
+
SOME_TOKEN=abc123
|
|
256
|
+
CLIENT_SECRET=secret789
|
|
257
|
+
USER_PASSWORD=pass123
|
|
258
|
+
REGULAR_VAR=not_included
|
|
259
|
+
HUD_API_URL=https://api.hud.ai
|
|
260
|
+
""")
|
|
261
|
+
|
|
262
|
+
result = _extract_dotenv_api_key_vars(env_dir)
|
|
263
|
+
|
|
264
|
+
# Should include only API-like variables
|
|
265
|
+
assert "OPENAI_API_KEY" in result
|
|
266
|
+
assert "ANTHROPIC_API_KEY" in result
|
|
267
|
+
assert "SOME_TOKEN" in result
|
|
268
|
+
assert "CLIENT_SECRET" in result
|
|
269
|
+
assert "USER_PASSWORD" in result
|
|
270
|
+
assert "REGULAR_VAR" not in result
|
|
271
|
+
assert "HUD_API_URL" in result # API in name, so it's included
|
|
272
|
+
|
|
273
|
+
def test_is_remote_url(self):
|
|
274
|
+
"""Test remote URL detection."""
|
|
275
|
+
from hud.cli.flows.tasks import _is_remote_url
|
|
276
|
+
|
|
277
|
+
# This function matches URLs with domain names (not localhost or IPs)
|
|
278
|
+
assert _is_remote_url("https://mcp.hud.ai")
|
|
279
|
+
assert _is_remote_url("http://mcp.hud.ai")
|
|
280
|
+
assert _is_remote_url("https://mcp.hud.ai/some/path")
|
|
281
|
+
assert _is_remote_url("https://example.com") # Also matches
|
|
282
|
+
assert not _is_remote_url("http://localhost:8000") # localhost doesn't match
|
|
283
|
+
assert not _is_remote_url("file:///path/to/file") # file:// doesn't match
|
|
284
|
+
|
|
285
|
+
def test_extract_env_vars_from_docker_args(self):
|
|
286
|
+
"""Test extraction of environment variables from docker arguments."""
|
|
287
|
+
from hud.cli.flows.tasks import _extract_env_vars_from_docker_args
|
|
288
|
+
|
|
289
|
+
# Test with various docker arg formats
|
|
290
|
+
args = [
|
|
291
|
+
"run",
|
|
292
|
+
"--rm",
|
|
293
|
+
"-i",
|
|
294
|
+
"-e",
|
|
295
|
+
"VAR1",
|
|
296
|
+
"-e",
|
|
297
|
+
"VAR2=value",
|
|
298
|
+
"--env",
|
|
299
|
+
"VAR3",
|
|
300
|
+
"--env=VAR4",
|
|
301
|
+
# Note: -eFOO compact form is not supported by the implementation
|
|
302
|
+
"--env-file",
|
|
303
|
+
".env",
|
|
304
|
+
"-p",
|
|
305
|
+
"8080:80",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
result = _extract_env_vars_from_docker_args(args)
|
|
309
|
+
|
|
310
|
+
assert "VAR1" in result
|
|
311
|
+
assert "VAR2" in result
|
|
312
|
+
assert "VAR3" in result
|
|
313
|
+
assert "VAR4" in result
|
|
314
|
+
# FOO is not extracted because -eFOO compact form is not supported
|
|
315
|
+
assert len(result) == 4
|
|
316
|
+
|
|
317
|
+
def test_derive_remote_image(self):
|
|
318
|
+
"""Test deriving remote image from lock data."""
|
|
319
|
+
from hud.cli.flows.tasks import _derive_remote_image
|
|
320
|
+
|
|
321
|
+
# The function derives remote image from images.local, not images.remote
|
|
322
|
+
lock_data = {"images": {"local": "test-env:v1.0.0"}}
|
|
323
|
+
result = _derive_remote_image(lock_data)
|
|
324
|
+
assert result == "test-env:v1.0.0"
|
|
325
|
+
|
|
326
|
+
# Test fallback to legacy format
|
|
327
|
+
lock_data = {
|
|
328
|
+
"image": "test-org/test-env:v1.0.0",
|
|
329
|
+
}
|
|
330
|
+
result = _derive_remote_image(lock_data)
|
|
331
|
+
assert result == "test-org/test-env:v1.0.0"
|
|
332
|
+
|
|
333
|
+
def test_extract_vars_from_task_configs(self):
|
|
334
|
+
"""Test extraction of env vars from task configurations."""
|
|
335
|
+
from hud.cli.flows.tasks import _extract_vars_from_task_configs
|
|
336
|
+
|
|
337
|
+
raw_tasks = [
|
|
338
|
+
{
|
|
339
|
+
"prompt": "Task 1",
|
|
340
|
+
"mcp_config": {
|
|
341
|
+
"local": {"command": "docker", "args": ["run", "-e", "API_KEY1", "image1"]}
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"prompt": "Task 2",
|
|
346
|
+
"mcp_config": {
|
|
347
|
+
"local": {
|
|
348
|
+
"command": "docker",
|
|
349
|
+
"args": ["run", "-e", "API_KEY2", "--env", "API_KEY3", "image2"],
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
{"prompt": "Task 3", "mcp_config": {"remote": {"url": "https://mcp.hud.ai"}}},
|
|
354
|
+
]
|
|
355
|
+
|
|
356
|
+
result = _extract_vars_from_task_configs(raw_tasks)
|
|
357
|
+
|
|
358
|
+
assert "API_KEY1" in result
|
|
359
|
+
assert "API_KEY2" in result
|
|
360
|
+
assert "API_KEY3" in result
|
|
361
|
+
assert len(result) == 3
|
hud/cli/tests/test_debug.py
CHANGED
|
@@ -132,7 +132,7 @@ class TestDebugMCPStdio:
|
|
|
132
132
|
with (
|
|
133
133
|
patch("subprocess.run", return_value=mock_run_result),
|
|
134
134
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
135
|
-
patch("time.time", side_effect=[0, 0, 20]),
|
|
135
|
+
patch("hud.cli.debug.time.time", side_effect=[0, 0, 20]),
|
|
136
136
|
):
|
|
137
137
|
phases = await debug_mcp_stdio(["test-cmd"], logger, max_phase=5)
|
|
138
138
|
assert phases == 1
|
|
@@ -165,7 +165,7 @@ class TestDebugMCPStdio:
|
|
|
165
165
|
# Simulate timeout - time.time() is called multiple times in the loop
|
|
166
166
|
# Return increasing values to simulate time passing
|
|
167
167
|
time_values = list(range(20))
|
|
168
|
-
with patch("time.time", side_effect=time_values):
|
|
168
|
+
with patch("hud.cli.debug.time.time", side_effect=time_values):
|
|
169
169
|
phases = await debug_mcp_stdio(["test-cmd"], logger, max_phase=5)
|
|
170
170
|
assert phases == 1
|
|
171
171
|
output = logger.get_output()
|
|
@@ -207,7 +207,7 @@ class TestDebugMCPStdio:
|
|
|
207
207
|
with (
|
|
208
208
|
patch("subprocess.run", return_value=mock_run_result),
|
|
209
209
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
210
|
-
patch("hud.
|
|
210
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
211
211
|
):
|
|
212
212
|
mock_client = MockClient.return_value
|
|
213
213
|
mock_client.initialize = AsyncMock()
|
|
@@ -240,7 +240,7 @@ class TestDebugMCPStdio:
|
|
|
240
240
|
with (
|
|
241
241
|
patch("subprocess.run", return_value=mock_run_result),
|
|
242
242
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
243
|
-
patch("hud.
|
|
243
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
244
244
|
):
|
|
245
245
|
mock_client = MockClient.return_value
|
|
246
246
|
mock_client.initialize = AsyncMock()
|
|
@@ -277,7 +277,7 @@ class TestDebugMCPStdio:
|
|
|
277
277
|
with (
|
|
278
278
|
patch("subprocess.run", return_value=mock_run_result),
|
|
279
279
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
280
|
-
patch("hud.
|
|
280
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
281
281
|
):
|
|
282
282
|
mock_client = MockClient.return_value
|
|
283
283
|
mock_client.initialize = AsyncMock()
|
|
@@ -286,7 +286,9 @@ class TestDebugMCPStdio:
|
|
|
286
286
|
mock_client.call_tool = AsyncMock()
|
|
287
287
|
mock_client.shutdown = AsyncMock()
|
|
288
288
|
|
|
289
|
-
with patch(
|
|
289
|
+
with patch(
|
|
290
|
+
"hud.cli.debug.time.time", side_effect=[0, 5, 5, 5, 5]
|
|
291
|
+
): # Start at 0, then 5 for the rest
|
|
290
292
|
phases = await debug_mcp_stdio(["test-cmd"], logger, max_phase=4)
|
|
291
293
|
assert phases == 4
|
|
292
294
|
output = logger.get_output()
|
|
@@ -311,7 +313,7 @@ class TestDebugMCPStdio:
|
|
|
311
313
|
with (
|
|
312
314
|
patch("subprocess.run", return_value=mock_run_result),
|
|
313
315
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
314
|
-
patch("hud.
|
|
316
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
315
317
|
):
|
|
316
318
|
mock_client = MockClient.return_value
|
|
317
319
|
mock_client.initialize = AsyncMock()
|
|
@@ -324,7 +326,7 @@ class TestDebugMCPStdio:
|
|
|
324
326
|
|
|
325
327
|
# Simulate slow init (>30s)
|
|
326
328
|
# time.time() is called at start and after phase 3
|
|
327
|
-
with patch("time.time", side_effect=[0, 0, 0, 35, 35, 35]):
|
|
329
|
+
with patch("hud.cli.debug.time.time", side_effect=[0, 0, 0, 35, 35, 35]):
|
|
328
330
|
phases = await debug_mcp_stdio(["test-cmd"], logger, max_phase=5)
|
|
329
331
|
output = logger.get_output()
|
|
330
332
|
# Check if we got to phase 4 where the timing check happens
|
|
@@ -349,7 +351,7 @@ class TestDebugMCPStdio:
|
|
|
349
351
|
with (
|
|
350
352
|
patch("subprocess.run", return_value=mock_run_result),
|
|
351
353
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
352
|
-
patch("hud.
|
|
354
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
353
355
|
):
|
|
354
356
|
# Create different mock instances for each client
|
|
355
357
|
mock_clients = []
|
|
@@ -393,7 +395,7 @@ class TestDebugMCPStdio:
|
|
|
393
395
|
with (
|
|
394
396
|
patch("subprocess.run", return_value=mock_run_result),
|
|
395
397
|
patch("subprocess.Popen", return_value=mock_proc),
|
|
396
|
-
patch("hud.
|
|
398
|
+
patch("hud.clients.fastmcp.FastMCPHUDClient") as MockClient,
|
|
397
399
|
):
|
|
398
400
|
# Set up for phase 1-4 success first
|
|
399
401
|
test_tool = Mock()
|