hud-python 0.4.45__py3-none-any.whl → 0.5.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +70 -5
  3. hud/agents/base.py +238 -500
  4. hud/agents/claude.py +236 -247
  5. hud/agents/gateway.py +42 -0
  6. hud/agents/gemini.py +264 -0
  7. hud/agents/gemini_cua.py +324 -0
  8. hud/agents/grounded_openai.py +98 -100
  9. hud/agents/misc/integration_test_agent.py +51 -20
  10. hud/agents/misc/response_agent.py +48 -36
  11. hud/agents/openai.py +282 -296
  12. hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
  13. hud/agents/operator.py +199 -0
  14. hud/agents/resolver.py +70 -0
  15. hud/agents/tests/conftest.py +133 -0
  16. hud/agents/tests/test_base.py +300 -622
  17. hud/agents/tests/test_base_runtime.py +233 -0
  18. hud/agents/tests/test_claude.py +381 -214
  19. hud/agents/tests/test_client.py +9 -10
  20. hud/agents/tests/test_gemini.py +369 -0
  21. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  22. hud/agents/tests/test_openai.py +377 -140
  23. hud/agents/tests/test_operator.py +362 -0
  24. hud/agents/tests/test_resolver.py +192 -0
  25. hud/agents/tests/test_run_eval.py +179 -0
  26. hud/agents/types.py +148 -0
  27. hud/cli/__init__.py +493 -546
  28. hud/cli/analyze.py +43 -5
  29. hud/cli/build.py +699 -113
  30. hud/cli/debug.py +8 -5
  31. hud/cli/dev.py +889 -732
  32. hud/cli/eval.py +793 -667
  33. hud/cli/flows/dev.py +167 -0
  34. hud/cli/flows/init.py +191 -0
  35. hud/cli/flows/tasks.py +153 -56
  36. hud/cli/flows/templates.py +151 -0
  37. hud/cli/flows/tests/__init__.py +1 -0
  38. hud/cli/flows/tests/test_dev.py +126 -0
  39. hud/cli/init.py +60 -58
  40. hud/cli/pull.py +1 -1
  41. hud/cli/push.py +38 -13
  42. hud/cli/rft.py +311 -0
  43. hud/cli/rft_status.py +145 -0
  44. hud/cli/tests/test_analyze.py +5 -5
  45. hud/cli/tests/test_analyze_metadata.py +3 -2
  46. hud/cli/tests/test_analyze_module.py +120 -0
  47. hud/cli/tests/test_build.py +110 -8
  48. hud/cli/tests/test_build_failure.py +41 -0
  49. hud/cli/tests/test_build_module.py +50 -0
  50. hud/cli/tests/test_cli_init.py +6 -1
  51. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  52. hud/cli/tests/test_cli_root.py +140 -0
  53. hud/cli/tests/test_convert.py +361 -0
  54. hud/cli/tests/test_debug.py +12 -10
  55. hud/cli/tests/test_dev.py +197 -0
  56. hud/cli/tests/test_eval.py +251 -0
  57. hud/cli/tests/test_eval_bedrock.py +51 -0
  58. hud/cli/tests/test_init.py +124 -0
  59. hud/cli/tests/test_main_module.py +11 -5
  60. hud/cli/tests/test_mcp_server.py +12 -100
  61. hud/cli/tests/test_push.py +1 -1
  62. hud/cli/tests/test_push_happy.py +74 -0
  63. hud/cli/tests/test_push_wrapper.py +23 -0
  64. hud/cli/tests/test_registry.py +1 -1
  65. hud/cli/tests/test_utils.py +1 -1
  66. hud/cli/{rl → utils}/celebrate.py +14 -12
  67. hud/cli/utils/config.py +18 -1
  68. hud/cli/utils/docker.py +130 -4
  69. hud/cli/utils/env_check.py +9 -9
  70. hud/cli/utils/git.py +136 -0
  71. hud/cli/utils/interactive.py +39 -5
  72. hud/cli/utils/metadata.py +70 -1
  73. hud/cli/utils/runner.py +1 -1
  74. hud/cli/utils/server.py +2 -2
  75. hud/cli/utils/source_hash.py +3 -3
  76. hud/cli/utils/tasks.py +4 -1
  77. hud/cli/utils/tests/__init__.py +0 -0
  78. hud/cli/utils/tests/test_config.py +58 -0
  79. hud/cli/utils/tests/test_docker.py +93 -0
  80. hud/cli/utils/tests/test_docker_hints.py +71 -0
  81. hud/cli/utils/tests/test_env_check.py +74 -0
  82. hud/cli/utils/tests/test_environment.py +42 -0
  83. hud/cli/utils/tests/test_git.py +142 -0
  84. hud/cli/utils/tests/test_interactive_module.py +60 -0
  85. hud/cli/utils/tests/test_local_runner.py +50 -0
  86. hud/cli/utils/tests/test_logging_utils.py +23 -0
  87. hud/cli/utils/tests/test_metadata.py +49 -0
  88. hud/cli/utils/tests/test_package_runner.py +35 -0
  89. hud/cli/utils/tests/test_registry_utils.py +49 -0
  90. hud/cli/utils/tests/test_remote_runner.py +25 -0
  91. hud/cli/utils/tests/test_runner_modules.py +52 -0
  92. hud/cli/utils/tests/test_source_hash.py +36 -0
  93. hud/cli/utils/tests/test_tasks.py +80 -0
  94. hud/cli/utils/version_check.py +258 -0
  95. hud/cli/{rl → utils}/viewer.py +2 -2
  96. hud/clients/README.md +12 -11
  97. hud/clients/__init__.py +4 -3
  98. hud/clients/base.py +166 -26
  99. hud/clients/environment.py +51 -0
  100. hud/clients/fastmcp.py +13 -6
  101. hud/clients/mcp_use.py +45 -15
  102. hud/clients/tests/test_analyze_scenarios.py +206 -0
  103. hud/clients/tests/test_protocol.py +9 -3
  104. hud/datasets/__init__.py +23 -20
  105. hud/datasets/loader.py +326 -0
  106. hud/datasets/runner.py +198 -105
  107. hud/datasets/tests/__init__.py +0 -0
  108. hud/datasets/tests/test_loader.py +221 -0
  109. hud/datasets/tests/test_utils.py +315 -0
  110. hud/datasets/utils.py +270 -90
  111. hud/environment/__init__.py +52 -0
  112. hud/environment/connection.py +258 -0
  113. hud/environment/connectors/__init__.py +33 -0
  114. hud/environment/connectors/base.py +68 -0
  115. hud/environment/connectors/local.py +177 -0
  116. hud/environment/connectors/mcp_config.py +137 -0
  117. hud/environment/connectors/openai.py +101 -0
  118. hud/environment/connectors/remote.py +172 -0
  119. hud/environment/environment.py +835 -0
  120. hud/environment/integrations/__init__.py +45 -0
  121. hud/environment/integrations/adk.py +67 -0
  122. hud/environment/integrations/anthropic.py +196 -0
  123. hud/environment/integrations/gemini.py +92 -0
  124. hud/environment/integrations/langchain.py +82 -0
  125. hud/environment/integrations/llamaindex.py +68 -0
  126. hud/environment/integrations/openai.py +238 -0
  127. hud/environment/mock.py +306 -0
  128. hud/environment/router.py +263 -0
  129. hud/environment/scenarios.py +620 -0
  130. hud/environment/tests/__init__.py +1 -0
  131. hud/environment/tests/test_connection.py +317 -0
  132. hud/environment/tests/test_connectors.py +205 -0
  133. hud/environment/tests/test_environment.py +593 -0
  134. hud/environment/tests/test_integrations.py +257 -0
  135. hud/environment/tests/test_local_connectors.py +242 -0
  136. hud/environment/tests/test_scenarios.py +1086 -0
  137. hud/environment/tests/test_tools.py +208 -0
  138. hud/environment/types.py +23 -0
  139. hud/environment/utils/__init__.py +35 -0
  140. hud/environment/utils/formats.py +215 -0
  141. hud/environment/utils/schema.py +171 -0
  142. hud/environment/utils/tool_wrappers.py +113 -0
  143. hud/eval/__init__.py +67 -0
  144. hud/eval/context.py +727 -0
  145. hud/eval/display.py +299 -0
  146. hud/eval/instrument.py +187 -0
  147. hud/eval/manager.py +533 -0
  148. hud/eval/parallel.py +268 -0
  149. hud/eval/task.py +372 -0
  150. hud/eval/tests/__init__.py +1 -0
  151. hud/eval/tests/test_context.py +178 -0
  152. hud/eval/tests/test_eval.py +210 -0
  153. hud/eval/tests/test_manager.py +152 -0
  154. hud/eval/tests/test_parallel.py +168 -0
  155. hud/eval/tests/test_task.py +291 -0
  156. hud/eval/types.py +65 -0
  157. hud/eval/utils.py +194 -0
  158. hud/patches/__init__.py +19 -0
  159. hud/patches/mcp_patches.py +308 -0
  160. hud/patches/warnings.py +54 -0
  161. hud/samples/browser.py +4 -4
  162. hud/server/__init__.py +2 -1
  163. hud/server/low_level.py +2 -1
  164. hud/server/router.py +164 -0
  165. hud/server/server.py +567 -80
  166. hud/server/tests/test_mcp_server_integration.py +11 -11
  167. hud/server/tests/test_mcp_server_more.py +1 -1
  168. hud/server/tests/test_server_extra.py +2 -0
  169. hud/settings.py +45 -3
  170. hud/shared/exceptions.py +36 -10
  171. hud/shared/hints.py +26 -1
  172. hud/shared/requests.py +15 -3
  173. hud/shared/tests/test_exceptions.py +40 -31
  174. hud/shared/tests/test_hints.py +167 -0
  175. hud/telemetry/__init__.py +20 -19
  176. hud/telemetry/exporter.py +201 -0
  177. hud/telemetry/instrument.py +165 -253
  178. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  179. hud/telemetry/tests/test_exporter.py +258 -0
  180. hud/telemetry/tests/test_instrument.py +401 -0
  181. hud/tools/__init__.py +18 -2
  182. hud/tools/agent.py +223 -0
  183. hud/tools/apply_patch.py +639 -0
  184. hud/tools/base.py +54 -4
  185. hud/tools/bash.py +2 -2
  186. hud/tools/computer/__init__.py +36 -3
  187. hud/tools/computer/anthropic.py +2 -2
  188. hud/tools/computer/gemini.py +385 -0
  189. hud/tools/computer/hud.py +23 -6
  190. hud/tools/computer/openai.py +20 -21
  191. hud/tools/computer/qwen.py +434 -0
  192. hud/tools/computer/settings.py +37 -0
  193. hud/tools/edit.py +3 -7
  194. hud/tools/executors/base.py +4 -2
  195. hud/tools/executors/pyautogui.py +1 -1
  196. hud/tools/grounding/grounded_tool.py +13 -18
  197. hud/tools/grounding/grounder.py +10 -31
  198. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  199. hud/tools/jupyter.py +330 -0
  200. hud/tools/playwright.py +18 -3
  201. hud/tools/shell.py +308 -0
  202. hud/tools/tests/test_agent_tool.py +355 -0
  203. hud/tools/tests/test_apply_patch.py +718 -0
  204. hud/tools/tests/test_computer.py +4 -9
  205. hud/tools/tests/test_computer_actions.py +24 -2
  206. hud/tools/tests/test_jupyter_tool.py +181 -0
  207. hud/tools/tests/test_shell.py +596 -0
  208. hud/tools/tests/test_submit.py +85 -0
  209. hud/tools/tests/test_types.py +193 -0
  210. hud/tools/types.py +21 -1
  211. hud/types.py +194 -56
  212. hud/utils/__init__.py +2 -0
  213. hud/utils/env.py +67 -0
  214. hud/utils/hud_console.py +89 -18
  215. hud/utils/mcp.py +15 -58
  216. hud/utils/strict_schema.py +162 -0
  217. hud/utils/tests/test_init.py +1 -2
  218. hud/utils/tests/test_mcp.py +1 -28
  219. hud/utils/tests/test_pretty_errors.py +186 -0
  220. hud/utils/tests/test_tool_shorthand.py +154 -0
  221. hud/utils/tests/test_version.py +1 -1
  222. hud/utils/types.py +20 -0
  223. hud/version.py +1 -1
  224. hud_python-0.5.13.dist-info/METADATA +264 -0
  225. hud_python-0.5.13.dist-info/RECORD +305 -0
  226. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
  227. hud/agents/langchain.py +0 -261
  228. hud/agents/lite_llm.py +0 -72
  229. hud/cli/rl/__init__.py +0 -180
  230. hud/cli/rl/config.py +0 -101
  231. hud/cli/rl/display.py +0 -133
  232. hud/cli/rl/gpu.py +0 -63
  233. hud/cli/rl/gpu_utils.py +0 -321
  234. hud/cli/rl/local_runner.py +0 -595
  235. hud/cli/rl/presets.py +0 -96
  236. hud/cli/rl/remote_runner.py +0 -463
  237. hud/cli/rl/rl_api.py +0 -150
  238. hud/cli/rl/vllm.py +0 -177
  239. hud/cli/rl/wait_utils.py +0 -89
  240. hud/datasets/parallel.py +0 -687
  241. hud/misc/__init__.py +0 -1
  242. hud/misc/claude_plays_pokemon.py +0 -292
  243. hud/otel/__init__.py +0 -35
  244. hud/otel/collector.py +0 -142
  245. hud/otel/config.py +0 -181
  246. hud/otel/context.py +0 -570
  247. hud/otel/exporters.py +0 -369
  248. hud/otel/instrumentation.py +0 -135
  249. hud/otel/processors.py +0 -121
  250. hud/otel/tests/__init__.py +0 -1
  251. hud/otel/tests/test_processors.py +0 -197
  252. hud/rl/README.md +0 -30
  253. hud/rl/__init__.py +0 -1
  254. hud/rl/actor.py +0 -176
  255. hud/rl/buffer.py +0 -405
  256. hud/rl/chat_template.jinja +0 -101
  257. hud/rl/config.py +0 -192
  258. hud/rl/distributed.py +0 -132
  259. hud/rl/learner.py +0 -637
  260. hud/rl/tests/__init__.py +0 -1
  261. hud/rl/tests/test_learner.py +0 -186
  262. hud/rl/train.py +0 -382
  263. hud/rl/types.py +0 -101
  264. hud/rl/utils/start_vllm_server.sh +0 -30
  265. hud/rl/utils.py +0 -524
  266. hud/rl/vllm_adapter.py +0 -143
  267. hud/telemetry/job.py +0 -352
  268. hud/telemetry/replay.py +0 -74
  269. hud/telemetry/tests/test_replay.py +0 -40
  270. hud/telemetry/tests/test_trace.py +0 -63
  271. hud/telemetry/trace.py +0 -158
  272. hud/utils/agent_factories.py +0 -86
  273. hud/utils/async_utils.py +0 -65
  274. hud/utils/group_eval.py +0 -223
  275. hud/utils/progress.py +0 -149
  276. hud/utils/tasks.py +0 -127
  277. hud/utils/tests/test_async_utils.py +0 -173
  278. hud/utils/tests/test_progress.py +0 -261
  279. hud_python-0.4.45.dist-info/METADATA +0 -552
  280. hud_python-0.4.45.dist-info/RECORD +0 -228
  281. {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
  282. {hud_python-0.4.45.dist-info → hud_python-0.5.13.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
@@ -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.cli.debug.MCPClient") as MockClient,
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.cli.debug.MCPClient") as MockClient,
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.cli.debug.MCPClient") as MockClient,
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("time.time", side_effect=[0, 5, 5, 5, 5]): # Start at 0, then 5 for the rest
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.cli.debug.MCPClient") as MockClient,
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.cli.debug.MCPClient") as MockClient,
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.cli.debug.MCPClient") as MockClient,
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()