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.
Files changed (274) hide show
  1. hud/__init__.py +27 -7
  2. hud/agents/__init__.py +11 -5
  3. hud/agents/base.py +220 -500
  4. hud/agents/claude.py +200 -240
  5. hud/agents/gemini.py +275 -0
  6. hud/agents/gemini_cua.py +335 -0
  7. hud/agents/grounded_openai.py +98 -100
  8. hud/agents/misc/integration_test_agent.py +51 -20
  9. hud/agents/misc/response_agent.py +41 -36
  10. hud/agents/openai.py +291 -292
  11. hud/agents/{openai_chat_generic.py → openai_chat.py} +80 -34
  12. hud/agents/operator.py +211 -0
  13. hud/agents/tests/conftest.py +133 -0
  14. hud/agents/tests/test_base.py +300 -622
  15. hud/agents/tests/test_base_runtime.py +233 -0
  16. hud/agents/tests/test_claude.py +379 -210
  17. hud/agents/tests/test_client.py +9 -10
  18. hud/agents/tests/test_gemini.py +369 -0
  19. hud/agents/tests/test_grounded_openai_agent.py +65 -50
  20. hud/agents/tests/test_openai.py +376 -140
  21. hud/agents/tests/test_operator.py +362 -0
  22. hud/agents/tests/test_run_eval.py +179 -0
  23. hud/cli/__init__.py +461 -545
  24. hud/cli/analyze.py +43 -5
  25. hud/cli/build.py +664 -110
  26. hud/cli/debug.py +8 -5
  27. hud/cli/dev.py +882 -734
  28. hud/cli/eval.py +782 -668
  29. hud/cli/flows/dev.py +167 -0
  30. hud/cli/flows/init.py +191 -0
  31. hud/cli/flows/tasks.py +153 -56
  32. hud/cli/flows/templates.py +151 -0
  33. hud/cli/flows/tests/__init__.py +1 -0
  34. hud/cli/flows/tests/test_dev.py +126 -0
  35. hud/cli/init.py +60 -58
  36. hud/cli/push.py +29 -11
  37. hud/cli/rft.py +311 -0
  38. hud/cli/rft_status.py +145 -0
  39. hud/cli/tests/test_analyze.py +5 -5
  40. hud/cli/tests/test_analyze_metadata.py +3 -2
  41. hud/cli/tests/test_analyze_module.py +120 -0
  42. hud/cli/tests/test_build.py +108 -6
  43. hud/cli/tests/test_build_failure.py +41 -0
  44. hud/cli/tests/test_build_module.py +50 -0
  45. hud/cli/tests/test_cli_init.py +6 -1
  46. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  47. hud/cli/tests/test_cli_root.py +140 -0
  48. hud/cli/tests/test_convert.py +361 -0
  49. hud/cli/tests/test_debug.py +12 -10
  50. hud/cli/tests/test_dev.py +197 -0
  51. hud/cli/tests/test_eval.py +251 -0
  52. hud/cli/tests/test_eval_bedrock.py +51 -0
  53. hud/cli/tests/test_init.py +124 -0
  54. hud/cli/tests/test_main_module.py +11 -5
  55. hud/cli/tests/test_mcp_server.py +12 -100
  56. hud/cli/tests/test_push_happy.py +74 -0
  57. hud/cli/tests/test_push_wrapper.py +23 -0
  58. hud/cli/tests/test_registry.py +1 -1
  59. hud/cli/tests/test_utils.py +1 -1
  60. hud/cli/{rl → utils}/celebrate.py +14 -12
  61. hud/cli/utils/config.py +18 -1
  62. hud/cli/utils/docker.py +130 -4
  63. hud/cli/utils/env_check.py +9 -9
  64. hud/cli/utils/git.py +136 -0
  65. hud/cli/utils/interactive.py +39 -5
  66. hud/cli/utils/metadata.py +69 -0
  67. hud/cli/utils/runner.py +1 -1
  68. hud/cli/utils/server.py +2 -2
  69. hud/cli/utils/source_hash.py +3 -3
  70. hud/cli/utils/tasks.py +4 -1
  71. hud/cli/utils/tests/__init__.py +0 -0
  72. hud/cli/utils/tests/test_config.py +58 -0
  73. hud/cli/utils/tests/test_docker.py +93 -0
  74. hud/cli/utils/tests/test_docker_hints.py +71 -0
  75. hud/cli/utils/tests/test_env_check.py +74 -0
  76. hud/cli/utils/tests/test_environment.py +42 -0
  77. hud/cli/utils/tests/test_git.py +142 -0
  78. hud/cli/utils/tests/test_interactive_module.py +60 -0
  79. hud/cli/utils/tests/test_local_runner.py +50 -0
  80. hud/cli/utils/tests/test_logging_utils.py +23 -0
  81. hud/cli/utils/tests/test_metadata.py +49 -0
  82. hud/cli/utils/tests/test_package_runner.py +35 -0
  83. hud/cli/utils/tests/test_registry_utils.py +49 -0
  84. hud/cli/utils/tests/test_remote_runner.py +25 -0
  85. hud/cli/utils/tests/test_runner_modules.py +52 -0
  86. hud/cli/utils/tests/test_source_hash.py +36 -0
  87. hud/cli/utils/tests/test_tasks.py +80 -0
  88. hud/cli/utils/version_check.py +258 -0
  89. hud/cli/{rl → utils}/viewer.py +2 -2
  90. hud/clients/README.md +12 -11
  91. hud/clients/__init__.py +4 -3
  92. hud/clients/base.py +166 -26
  93. hud/clients/environment.py +51 -0
  94. hud/clients/fastmcp.py +13 -6
  95. hud/clients/mcp_use.py +40 -15
  96. hud/clients/tests/test_analyze_scenarios.py +206 -0
  97. hud/clients/tests/test_protocol.py +9 -3
  98. hud/datasets/__init__.py +23 -20
  99. hud/datasets/loader.py +327 -0
  100. hud/datasets/runner.py +192 -105
  101. hud/datasets/tests/__init__.py +0 -0
  102. hud/datasets/tests/test_loader.py +221 -0
  103. hud/datasets/tests/test_utils.py +315 -0
  104. hud/datasets/utils.py +270 -90
  105. hud/environment/__init__.py +50 -0
  106. hud/environment/connection.py +206 -0
  107. hud/environment/connectors/__init__.py +33 -0
  108. hud/environment/connectors/base.py +68 -0
  109. hud/environment/connectors/local.py +177 -0
  110. hud/environment/connectors/mcp_config.py +109 -0
  111. hud/environment/connectors/openai.py +101 -0
  112. hud/environment/connectors/remote.py +172 -0
  113. hud/environment/environment.py +694 -0
  114. hud/environment/integrations/__init__.py +45 -0
  115. hud/environment/integrations/adk.py +67 -0
  116. hud/environment/integrations/anthropic.py +196 -0
  117. hud/environment/integrations/gemini.py +92 -0
  118. hud/environment/integrations/langchain.py +82 -0
  119. hud/environment/integrations/llamaindex.py +68 -0
  120. hud/environment/integrations/openai.py +238 -0
  121. hud/environment/mock.py +306 -0
  122. hud/environment/router.py +112 -0
  123. hud/environment/scenarios.py +493 -0
  124. hud/environment/tests/__init__.py +1 -0
  125. hud/environment/tests/test_connection.py +317 -0
  126. hud/environment/tests/test_connectors.py +218 -0
  127. hud/environment/tests/test_environment.py +161 -0
  128. hud/environment/tests/test_integrations.py +257 -0
  129. hud/environment/tests/test_local_connectors.py +201 -0
  130. hud/environment/tests/test_scenarios.py +280 -0
  131. hud/environment/tests/test_tools.py +208 -0
  132. hud/environment/types.py +23 -0
  133. hud/environment/utils/__init__.py +35 -0
  134. hud/environment/utils/formats.py +215 -0
  135. hud/environment/utils/schema.py +171 -0
  136. hud/environment/utils/tool_wrappers.py +113 -0
  137. hud/eval/__init__.py +67 -0
  138. hud/eval/context.py +674 -0
  139. hud/eval/display.py +299 -0
  140. hud/eval/instrument.py +185 -0
  141. hud/eval/manager.py +466 -0
  142. hud/eval/parallel.py +268 -0
  143. hud/eval/task.py +340 -0
  144. hud/eval/tests/__init__.py +1 -0
  145. hud/eval/tests/test_context.py +178 -0
  146. hud/eval/tests/test_eval.py +210 -0
  147. hud/eval/tests/test_manager.py +152 -0
  148. hud/eval/tests/test_parallel.py +168 -0
  149. hud/eval/tests/test_task.py +145 -0
  150. hud/eval/types.py +63 -0
  151. hud/eval/utils.py +183 -0
  152. hud/patches/__init__.py +19 -0
  153. hud/patches/mcp_patches.py +151 -0
  154. hud/patches/warnings.py +54 -0
  155. hud/samples/browser.py +4 -4
  156. hud/server/__init__.py +2 -1
  157. hud/server/low_level.py +2 -1
  158. hud/server/router.py +164 -0
  159. hud/server/server.py +567 -80
  160. hud/server/tests/test_mcp_server_integration.py +11 -11
  161. hud/server/tests/test_mcp_server_more.py +1 -1
  162. hud/server/tests/test_server_extra.py +2 -0
  163. hud/settings.py +45 -3
  164. hud/shared/exceptions.py +36 -10
  165. hud/shared/hints.py +26 -1
  166. hud/shared/requests.py +15 -3
  167. hud/shared/tests/test_exceptions.py +40 -31
  168. hud/shared/tests/test_hints.py +167 -0
  169. hud/telemetry/__init__.py +20 -19
  170. hud/telemetry/exporter.py +201 -0
  171. hud/telemetry/instrument.py +158 -253
  172. hud/telemetry/tests/test_eval_telemetry.py +356 -0
  173. hud/telemetry/tests/test_exporter.py +258 -0
  174. hud/telemetry/tests/test_instrument.py +401 -0
  175. hud/tools/__init__.py +16 -2
  176. hud/tools/apply_patch.py +639 -0
  177. hud/tools/base.py +54 -4
  178. hud/tools/bash.py +2 -2
  179. hud/tools/computer/__init__.py +4 -0
  180. hud/tools/computer/anthropic.py +2 -2
  181. hud/tools/computer/gemini.py +385 -0
  182. hud/tools/computer/hud.py +23 -6
  183. hud/tools/computer/openai.py +20 -21
  184. hud/tools/computer/qwen.py +434 -0
  185. hud/tools/computer/settings.py +37 -0
  186. hud/tools/edit.py +3 -7
  187. hud/tools/executors/base.py +4 -2
  188. hud/tools/executors/pyautogui.py +1 -1
  189. hud/tools/grounding/grounded_tool.py +13 -18
  190. hud/tools/grounding/grounder.py +10 -31
  191. hud/tools/grounding/tests/test_grounded_tool.py +26 -44
  192. hud/tools/jupyter.py +330 -0
  193. hud/tools/playwright.py +18 -3
  194. hud/tools/shell.py +308 -0
  195. hud/tools/tests/test_apply_patch.py +718 -0
  196. hud/tools/tests/test_computer.py +4 -9
  197. hud/tools/tests/test_computer_actions.py +24 -2
  198. hud/tools/tests/test_jupyter_tool.py +181 -0
  199. hud/tools/tests/test_shell.py +596 -0
  200. hud/tools/tests/test_submit.py +85 -0
  201. hud/tools/tests/test_types.py +193 -0
  202. hud/tools/types.py +21 -1
  203. hud/types.py +167 -57
  204. hud/utils/__init__.py +2 -0
  205. hud/utils/env.py +67 -0
  206. hud/utils/hud_console.py +61 -3
  207. hud/utils/mcp.py +15 -58
  208. hud/utils/strict_schema.py +162 -0
  209. hud/utils/tests/test_init.py +1 -2
  210. hud/utils/tests/test_mcp.py +1 -28
  211. hud/utils/tests/test_pretty_errors.py +186 -0
  212. hud/utils/tests/test_tool_shorthand.py +154 -0
  213. hud/utils/tests/test_version.py +1 -1
  214. hud/utils/types.py +20 -0
  215. hud/version.py +1 -1
  216. hud_python-0.5.1.dist-info/METADATA +264 -0
  217. hud_python-0.5.1.dist-info/RECORD +299 -0
  218. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/WHEEL +1 -1
  219. hud/agents/langchain.py +0 -261
  220. hud/agents/lite_llm.py +0 -72
  221. hud/cli/rl/__init__.py +0 -180
  222. hud/cli/rl/config.py +0 -101
  223. hud/cli/rl/display.py +0 -133
  224. hud/cli/rl/gpu.py +0 -63
  225. hud/cli/rl/gpu_utils.py +0 -321
  226. hud/cli/rl/local_runner.py +0 -595
  227. hud/cli/rl/presets.py +0 -96
  228. hud/cli/rl/remote_runner.py +0 -463
  229. hud/cli/rl/rl_api.py +0 -150
  230. hud/cli/rl/vllm.py +0 -177
  231. hud/cli/rl/wait_utils.py +0 -89
  232. hud/datasets/parallel.py +0 -687
  233. hud/misc/__init__.py +0 -1
  234. hud/misc/claude_plays_pokemon.py +0 -292
  235. hud/otel/__init__.py +0 -35
  236. hud/otel/collector.py +0 -142
  237. hud/otel/config.py +0 -181
  238. hud/otel/context.py +0 -570
  239. hud/otel/exporters.py +0 -369
  240. hud/otel/instrumentation.py +0 -135
  241. hud/otel/processors.py +0 -121
  242. hud/otel/tests/__init__.py +0 -1
  243. hud/otel/tests/test_processors.py +0 -197
  244. hud/rl/README.md +0 -30
  245. hud/rl/__init__.py +0 -1
  246. hud/rl/actor.py +0 -176
  247. hud/rl/buffer.py +0 -405
  248. hud/rl/chat_template.jinja +0 -101
  249. hud/rl/config.py +0 -192
  250. hud/rl/distributed.py +0 -132
  251. hud/rl/learner.py +0 -637
  252. hud/rl/tests/__init__.py +0 -1
  253. hud/rl/tests/test_learner.py +0 -186
  254. hud/rl/train.py +0 -382
  255. hud/rl/types.py +0 -101
  256. hud/rl/utils/start_vllm_server.sh +0 -30
  257. hud/rl/utils.py +0 -524
  258. hud/rl/vllm_adapter.py +0 -143
  259. hud/telemetry/job.py +0 -352
  260. hud/telemetry/replay.py +0 -74
  261. hud/telemetry/tests/test_replay.py +0 -40
  262. hud/telemetry/tests/test_trace.py +0 -63
  263. hud/telemetry/trace.py +0 -158
  264. hud/utils/agent_factories.py +0 -86
  265. hud/utils/async_utils.py +0 -65
  266. hud/utils/group_eval.py +0 -223
  267. hud/utils/progress.py +0 -149
  268. hud/utils/tasks.py +0 -127
  269. hud/utils/tests/test_async_utils.py +0 -173
  270. hud/utils/tests/test_progress.py +0 -261
  271. hud_python-0.4.45.dist-info/METADATA +0 -552
  272. hud_python-0.4.45.dist-info/RECORD +0 -228
  273. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/entry_points.txt +0 -0
  274. {hud_python-0.4.45.dist-info → hud_python-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,19 +3,21 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- from typing import Any
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  import questionary
9
- from mcp.types import TextContent
9
+ from mcp.types import ImageContent, TextContent
10
10
  from rich.console import Console
11
11
  from rich.panel import Panel
12
12
  from rich.prompt import Prompt
13
13
  from rich.syntax import Syntax
14
14
  from rich.tree import Tree
15
15
 
16
- from hud.clients import MCPClient
17
16
  from hud.utils.hud_console import HUDConsole
18
17
 
18
+ if TYPE_CHECKING:
19
+ from hud.clients import MCPClient
20
+
19
21
  console = Console()
20
22
 
21
23
 
@@ -38,6 +40,9 @@ class InteractiveMCPTester:
38
40
  async def connect(self) -> bool:
39
41
  """Connect to the MCP server."""
40
42
  try:
43
+ # Lazy import to avoid loading mcp_use on simple CLI commands
44
+ from hud.clients import MCPClient
45
+
41
46
  # Create MCP config for HTTP transport
42
47
  # Note: We explicitly set auth to None to prevent OAuth discovery attempts
43
48
  config = {"server": {"url": self.server_url, "auth": None}}
@@ -45,7 +50,6 @@ class InteractiveMCPTester:
45
50
  self.client = MCPClient(
46
51
  mcp_config=config,
47
52
  verbose=self.verbose,
48
- auto_trace=False, # Disable telemetry for interactive testing
49
53
  )
50
54
  await self.client.initialize()
51
55
 
@@ -242,7 +246,27 @@ class InteractiveMCPTester:
242
246
  # Prompt for each property
243
247
  args = {}
244
248
  for prop_name, prop_schema in properties.items():
245
- prop_type = prop_schema.get("type", "string")
249
+ prop_type = prop_schema.get("type")
250
+ if not prop_type and "anyOf" in prop_schema:
251
+ prop_type = next(
252
+ (
253
+ s.get("type")
254
+ for s in prop_schema.get("anyOf", [])
255
+ if s.get("type") != "null"
256
+ ),
257
+ None,
258
+ )
259
+ if not prop_type and "oneOf" in prop_schema:
260
+ prop_type = next(
261
+ (
262
+ s.get("type")
263
+ for s in prop_schema.get("oneOf", [])
264
+ if s.get("type") != "null"
265
+ ),
266
+ None,
267
+ )
268
+ prop_type = prop_type or "string"
269
+
246
270
  description = prop_schema.get("description", "")
247
271
  is_required = prop_name in required
248
272
 
@@ -353,6 +377,16 @@ class InteractiveMCPTester:
353
377
  border_style="green" if not result.isError else "red",
354
378
  )
355
379
  )
380
+ elif isinstance(content, ImageContent):
381
+ mime_type = getattr(content, "mimeType", "image/png")
382
+ data_length = len(content.data) if hasattr(content, "data") else 0
383
+ console.print(
384
+ Panel(
385
+ f"📷 Image ({mime_type})\nSize: {data_length:,} bytes (base64 encoded)",
386
+ title="Result",
387
+ border_style="green" if not result.isError else "red",
388
+ )
389
+ )
356
390
  else:
357
391
  # Handle other content types
358
392
  console.print(json.dumps(content, indent=2))
hud/cli/utils/metadata.py CHANGED
@@ -173,6 +173,8 @@ async def analyze_from_metadata(reference: str, output_format: str, verbose: boo
173
173
  "tools": [],
174
174
  "resources": [],
175
175
  "prompts": [],
176
+ "scenarios": [],
177
+ "verbose": verbose,
176
178
  }
177
179
 
178
180
  # Add basic info
@@ -206,6 +208,73 @@ async def analyze_from_metadata(reference: str, output_format: str, verbose: boo
206
208
  }
207
209
  )
208
210
 
211
+ # Extract resources
212
+ if "resources" in lock_data:
213
+ for resource in lock_data["resources"]:
214
+ analysis["resources"].append(
215
+ {
216
+ "uri": resource.get("uri", ""),
217
+ "name": resource.get("name", ""),
218
+ "description": resource.get("description", ""),
219
+ "mime_type": resource.get("mimeType", resource.get("mime_type", "")),
220
+ }
221
+ )
222
+
223
+ # Extract prompts
224
+ if "prompts" in lock_data:
225
+ for prompt in lock_data["prompts"]:
226
+ analysis["prompts"].append(
227
+ {
228
+ "name": prompt.get("name", ""),
229
+ "description": prompt.get("description", ""),
230
+ "arguments": prompt.get("arguments", []),
231
+ }
232
+ )
233
+
234
+ # Derive scenarios from scenario prompts/resources if present
235
+ scenarios_by_id: dict[str, dict] = {}
236
+ for p in analysis["prompts"]:
237
+ desc = (p.get("description") or "").strip()
238
+ if not desc.startswith("[Setup]"):
239
+ continue
240
+ scenario_id = p.get("name")
241
+ if not scenario_id:
242
+ continue
243
+ env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
244
+ scenarios_by_id[scenario_id] = {
245
+ "id": scenario_id,
246
+ "env": env_name,
247
+ "name": scenario_name or scenario_id,
248
+ "setup_description": desc,
249
+ "arguments": p.get("arguments") or [],
250
+ "has_setup_prompt": True,
251
+ "has_evaluate_resource": False,
252
+ }
253
+ for r in analysis["resources"]:
254
+ desc = (r.get("description") or "").strip()
255
+ if not desc.startswith("[Evaluate]"):
256
+ continue
257
+ scenario_id = r.get("uri")
258
+ if not scenario_id:
259
+ continue
260
+ env_name, scenario_name = ([*scenario_id.split(":", 1), ""])[:2]
261
+ if scenario_id not in scenarios_by_id:
262
+ scenarios_by_id[scenario_id] = {
263
+ "id": scenario_id,
264
+ "env": env_name,
265
+ "name": scenario_name or scenario_id,
266
+ "arguments": [],
267
+ "has_setup_prompt": False,
268
+ "has_evaluate_resource": True,
269
+ }
270
+ scenarios_by_id[scenario_id]["evaluate_description"] = desc
271
+ scenarios_by_id[scenario_id]["has_evaluate_resource"] = True
272
+
273
+ analysis["scenarios"] = sorted(
274
+ scenarios_by_id.values(),
275
+ key=lambda s: (str(s.get("env") or ""), str(s.get("name") or "")),
276
+ )
277
+
209
278
  # Display results
210
279
  hud_console.info("")
211
280
  if source == "local":
hud/cli/utils/runner.py CHANGED
@@ -16,7 +16,7 @@ def run_stdio_server(image: str, docker_args: list[str], verbose: bool) -> None:
16
16
  """Run Docker image as stdio MCP server (direct passthrough)."""
17
17
  hud_console = HUDConsole() # Use stderr for stdio mode
18
18
 
19
- # Build docker command
19
+ # Build docker command (image-only mode: do not auto-inject local .env)
20
20
  docker_cmd = ["docker", "run", "--rm", "-i", *docker_args, image]
21
21
 
22
22
  if verbose:
hud/cli/utils/server.py CHANGED
@@ -138,9 +138,9 @@ class MCPServerManager:
138
138
  logging.getLogger("uvicorn.access").setLevel(logging.ERROR)
139
139
  logging.getLogger("uvicorn.error").setLevel(logging.ERROR)
140
140
 
141
- import warnings
141
+ from hud.patches.warnings import apply_default_warning_filters
142
142
 
143
- warnings.filterwarnings("ignore", category=DeprecationWarning)
143
+ apply_default_warning_filters(verbose=False)
144
144
 
145
145
  try:
146
146
  await proxy.run_async(
@@ -1,7 +1,7 @@
1
1
  """Utilities to compute a fast, deterministic source hash for environments.
2
2
 
3
3
  This intentionally focuses on the typical HUD environment layout and aims to be fast:
4
- - Always include: Dockerfile, pyproject.toml
4
+ - Always include: Dockerfile.hud, Dockerfile, pyproject.toml
5
5
  - Include directories: controller/, environment/, src/
6
6
  - Exclude common build/runtime caches and lock files
7
7
 
@@ -40,8 +40,8 @@ EXCLUDE_FILES = {
40
40
  "hud.lock.yaml",
41
41
  }
42
42
 
43
- INCLUDE_FILES = {"Dockerfile", "pyproject.toml"}
44
- INCLUDE_DIRS = {"controller", "environment"}
43
+ INCLUDE_FILES = {"Dockerfile", "Dockerfile.hud", "pyproject.toml"}
44
+ INCLUDE_DIRS = {"server", "mcp", "controller", "environment"}
45
45
 
46
46
 
47
47
  def iter_source_files(root: Path) -> Iterable[Path]:
hud/cli/utils/tasks.py CHANGED
@@ -18,9 +18,12 @@ def find_tasks_file(tasks_file: str | None, msg: str = "Select a tasks file") ->
18
18
  ]
19
19
  all_files = [file for file in all_files if file[0] != "."] # Remove all config files
20
20
 
21
+ if not all_files:
22
+ # No task files found - raise a clear exception
23
+ raise FileNotFoundError("No task JSON or JSONL files found in current directory")
24
+
21
25
  if len(all_files) == 1:
22
26
  return str(all_files[0])
23
-
24
27
  else:
25
28
  # Prompt user to select a file
26
29
  return hud_console.select(msg, choices=all_files)
File without changes
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from hud.cli.utils.config import (
6
+ ensure_config_dir,
7
+ get_config_dir,
8
+ get_user_env_path,
9
+ load_env_file,
10
+ parse_env_file,
11
+ render_env_file,
12
+ save_env_file,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from pathlib import Path
17
+
18
+
19
+ def test_parse_env_file_basic():
20
+ contents = """
21
+ # comment
22
+ KEY=VALUE
23
+ EMPTY=
24
+ NOEQ
25
+ SPACED = v
26
+ """ # noqa: W291
27
+ data = parse_env_file(contents)
28
+ assert data["KEY"] == "VALUE"
29
+ assert data["EMPTY"] == ""
30
+ assert data["SPACED"] == "v"
31
+ assert "NOEQ" not in data
32
+
33
+
34
+ def test_render_and_load_roundtrip(tmp_path: Path):
35
+ env = {"A": "1", "B": "2"}
36
+ file_path = tmp_path / ".env"
37
+ rendered = render_env_file(env)
38
+ file_path.write_text(rendered, encoding="utf-8")
39
+ loaded = load_env_file(file_path)
40
+ assert loaded == env
41
+
42
+
43
+ def test_get_paths(monkeypatch, tmp_path: Path):
44
+ from pathlib import Path as _Path
45
+
46
+ monkeypatch.setattr(_Path, "home", lambda: tmp_path)
47
+ cfg = get_config_dir()
48
+ assert str(cfg).replace("\\", "/").endswith("/.hud")
49
+ assert str(get_user_env_path()).replace("\\", "/").endswith("/.hud/.env")
50
+
51
+
52
+ def test_ensure_and_save(tmp_path: Path, monkeypatch):
53
+ monkeypatch.setenv("HOME", str(tmp_path))
54
+ cfg = ensure_config_dir()
55
+ assert cfg.exists()
56
+ out = save_env_file({"K": "V"})
57
+ assert out.exists()
58
+ assert load_env_file(out) == {"K": "V"}
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from hud.cli.utils.docker import (
8
+ build_run_command,
9
+ generate_container_name,
10
+ get_docker_cmd,
11
+ image_exists,
12
+ remove_container,
13
+ require_docker_running,
14
+ )
15
+
16
+
17
+ def test_build_run_command_basic():
18
+ cmd = build_run_command("my-image:latest")
19
+ assert cmd[:4] == ["docker", "run", "--rm", "-i"]
20
+ assert cmd[-1] == "my-image:latest"
21
+
22
+
23
+ def test_build_run_command_with_args():
24
+ cmd = build_run_command("img", ["-e", "K=V", "-p", "8080:8080"])
25
+ assert "-e" in cmd and "K=V" in cmd
26
+ assert "-p" in cmd and "8080:8080" in cmd
27
+ assert cmd[-1] == "img"
28
+
29
+
30
+ def test_generate_container_name():
31
+ assert generate_container_name("repo/name:tag") == "hud-repo-name-tag"
32
+ assert generate_container_name("a/b:c", prefix="x") == "x-a-b-c"
33
+
34
+
35
+ @patch("subprocess.run")
36
+ def test_image_exists_true(mock_run):
37
+ mock_run.return_value = MagicMock(returncode=0)
38
+ assert image_exists("any") is True
39
+
40
+
41
+ @patch("subprocess.run")
42
+ def test_image_exists_false(mock_run):
43
+ mock_run.return_value = MagicMock(returncode=1)
44
+ assert image_exists("any") is False
45
+
46
+
47
+ @patch("subprocess.run")
48
+ def test_get_docker_cmd_success(mock_run):
49
+ mock_run.return_value = MagicMock(
50
+ stdout='[{"Config": {"Cmd": ["python", "-m", "app"]}}]', returncode=0
51
+ )
52
+ assert get_docker_cmd("img") == ["python", "-m", "app"]
53
+
54
+
55
+ @patch("subprocess.run")
56
+ def test_get_docker_cmd_none(mock_run):
57
+ mock_run.return_value = MagicMock(stdout="[]", returncode=0)
58
+ assert get_docker_cmd("img") is None
59
+
60
+
61
+ @patch("subprocess.run")
62
+ def test_remove_container_ok(mock_run):
63
+ mock_run.return_value = MagicMock(returncode=0)
64
+ assert remove_container("x") is True
65
+
66
+
67
+ @patch("shutil.which", return_value=None)
68
+ def test_require_docker_running_no_cli(_which):
69
+ import typer
70
+
71
+ with pytest.raises(typer.Exit):
72
+ require_docker_running()
73
+
74
+
75
+ @patch("shutil.which", return_value="docker")
76
+ @patch("subprocess.run")
77
+ def test_require_docker_running_ok(mock_run, _which):
78
+ mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
79
+ require_docker_running() # should not raise
80
+
81
+
82
+ @patch("shutil.which", return_value="docker")
83
+ @patch("subprocess.run")
84
+ def test_require_docker_running_error_emits_hints(mock_run, _which):
85
+ import typer
86
+
87
+ mock_run.return_value = MagicMock(
88
+ returncode=1,
89
+ stdout="Cannot connect to the Docker daemon",
90
+ stderr="",
91
+ )
92
+ with pytest.raises(typer.Exit):
93
+ require_docker_running()
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ from hud.cli.utils import docker as mod
8
+
9
+ pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Prefers Linux")
10
+
11
+
12
+ def test_emit_docker_hints_windows(monkeypatch):
13
+ # Patch the global hud_console used by hint printing
14
+
15
+ fake = type(
16
+ "C",
17
+ (),
18
+ {
19
+ "error": lambda *a, **k: None,
20
+ "hint": lambda *a, **k: None,
21
+ "dim_info": lambda *a, **k: None,
22
+ },
23
+ )()
24
+ monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
25
+ monkeypatch.setattr(mod.platform, "system", lambda: "Windows")
26
+ mod._emit_docker_hints("cannot connect to the docker daemon")
27
+
28
+
29
+ def test_emit_docker_hints_linux(monkeypatch):
30
+ fake = type(
31
+ "C",
32
+ (),
33
+ {
34
+ "error": lambda *a, **k: None,
35
+ "hint": lambda *a, **k: None,
36
+ "dim_info": lambda *a, **k: None,
37
+ },
38
+ )()
39
+ monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
40
+ monkeypatch.setattr(mod.platform, "system", lambda: "Linux")
41
+ mod._emit_docker_hints("Cannot connect to the Docker daemon")
42
+
43
+
44
+ def test_emit_docker_hints_darwin(monkeypatch):
45
+ fake = type(
46
+ "C",
47
+ (),
48
+ {
49
+ "error": lambda *a, **k: None,
50
+ "hint": lambda *a, **k: None,
51
+ "dim_info": lambda *a, **k: None,
52
+ },
53
+ )()
54
+ monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
55
+ monkeypatch.setattr(mod.platform, "system", lambda: "Darwin")
56
+ mod._emit_docker_hints("error during connect: is the docker daemon running")
57
+
58
+
59
+ def test_emit_docker_hints_generic(monkeypatch):
60
+ fake = type(
61
+ "C",
62
+ (),
63
+ {
64
+ "error": lambda *a, **k: None,
65
+ "hint": lambda *a, **k: None,
66
+ "dim_info": lambda *a, **k: None,
67
+ },
68
+ )()
69
+ monkeypatch.setattr("hud.utils.hud_console.hud_console", fake, raising=False)
70
+ monkeypatch.setattr(mod.platform, "system", lambda: "Other")
71
+ mod._emit_docker_hints("some unrelated error")
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime, timedelta
4
+ from typing import TYPE_CHECKING
5
+ from unittest.mock import patch
6
+
7
+ from hud.cli.utils.env_check import (
8
+ _collect_source_diffs,
9
+ _parse_generated_at,
10
+ ensure_built,
11
+ find_environment_dir,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+
18
+ def test_parse_generated_at_variants():
19
+ ts = _parse_generated_at({"build": {"generatedAt": datetime.now(UTC).isoformat()}})
20
+ assert isinstance(ts, float)
21
+ assert _parse_generated_at({}) is None
22
+
23
+
24
+ def test_collect_source_diffs_basic(tmp_path: Path):
25
+ env = tmp_path / "env"
26
+ env.mkdir()
27
+ # simulate files
28
+ (env / "Dockerfile").write_text("FROM python:3.11")
29
+ (env / "pyproject.toml").write_text("[tool.hud]")
30
+ (env / "a.txt").write_text("x")
31
+
32
+ # stored file list includes a non-existent file and old time
33
+ built_time = (datetime.now(UTC) - timedelta(days=1)).isoformat()
34
+ lock = {"build": {"sourceFiles": ["a.txt", "b.txt"], "generatedAt": built_time}}
35
+
36
+ # Patch list_source_files to return current env files
37
+ with patch("hud.cli.utils.env_check.list_source_files") as mock_list:
38
+ mock_list.return_value = [env / "a.txt", env / "Dockerfile"]
39
+ diffs = _collect_source_diffs(env, lock)
40
+ assert "Dockerfile" in diffs["added"]
41
+ assert "b.txt" in diffs["removed"]
42
+ assert "a.txt" in diffs["modified"] or "a.txt" in diffs["added"]
43
+
44
+
45
+ def test_find_environment_dir_prefers_lock(tmp_path: Path):
46
+ # Create env as a sibling to tasks, so it will be in the candidates list
47
+ parent = tmp_path / "parent"
48
+ parent.mkdir()
49
+ tasks = parent / "tasks.json"
50
+ tasks.write_text("[]")
51
+ env = tmp_path / "env"
52
+ env.mkdir()
53
+ (env / "hud.lock.yaml").write_text("version: 1.3")
54
+ # Set cwd to env so it's in the candidate list
55
+ with patch("pathlib.Path.cwd", return_value=env):
56
+ found = find_environment_dir(tasks)
57
+ # Should find env because cwd returns env and it has hud.lock.yaml
58
+ assert found == env
59
+
60
+
61
+ def test_ensure_built_no_lock_noninteractive(tmp_path: Path):
62
+ env = tmp_path / "e"
63
+ env.mkdir()
64
+ # Non-interactive: returns empty dict and does not raise
65
+ result = ensure_built(env, interactive=False)
66
+ assert result == {}
67
+
68
+
69
+ def test_ensure_built_interactive_build(tmp_path: Path):
70
+ env = tmp_path / "e"
71
+ env.mkdir()
72
+ # Simulate interactive=False path avoids prompts
73
+ result = ensure_built(env, interactive=False)
74
+ assert result == {}
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from unittest.mock import MagicMock, patch
5
+
6
+ from hud.cli.utils.environment import get_image_name, image_exists, is_environment_directory
7
+
8
+ if TYPE_CHECKING:
9
+ from pathlib import Path
10
+
11
+
12
+ def test_get_image_name_override():
13
+ name, source = get_image_name(".", image_override="custom:dev")
14
+ assert name == "custom:dev" and source == "override"
15
+
16
+
17
+ def test_get_image_name_auto(tmp_path: Path):
18
+ env = tmp_path / "my_env"
19
+ env.mkdir()
20
+ # Provide Dockerfile and pyproject to pass directory check later if used
21
+ (env / "Dockerfile").write_text("FROM python:3.11")
22
+ (env / "pyproject.toml").write_text("[tool.hud]\nimage='x'")
23
+ name, source = get_image_name(env)
24
+ # Because pyproject exists with image key, source should be cache
25
+ assert source == "cache"
26
+ assert name == "x"
27
+
28
+
29
+ def test_is_environment_directory(tmp_path: Path):
30
+ d = tmp_path / "env"
31
+ d.mkdir()
32
+ assert is_environment_directory(d) is False
33
+ (d / "Dockerfile").write_text("FROM python:3.11")
34
+ assert is_environment_directory(d) is False
35
+ (d / "pyproject.toml").write_text("[tool.hud]")
36
+ assert is_environment_directory(d) is True
37
+
38
+
39
+ @patch("subprocess.run")
40
+ def test_image_exists_true(mock_run):
41
+ mock_run.return_value = MagicMock(returncode=0)
42
+ assert image_exists("img") is True