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,257 @@
1
+ """Tests for format integrations - OpenAI, Anthropic, Gemini."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import mcp.types as mcp_types
8
+
9
+
10
+ def create_mock_tool(
11
+ name: str, description: str = "", schema: dict | None = None
12
+ ) -> mcp_types.Tool:
13
+ """Create a mock MCP tool for testing."""
14
+ return mcp_types.Tool(
15
+ name=name,
16
+ description=description,
17
+ inputSchema=schema or {"type": "object", "properties": {}},
18
+ )
19
+
20
+
21
+ class TestOpenAIMixin:
22
+ """Tests for OpenAI format conversion."""
23
+
24
+ def test_as_openai_chat_tools_basic(self) -> None:
25
+ """as_openai_chat_tools converts MCP tools to OpenAI format."""
26
+ from hud.environment.integrations.openai import OpenAIMixin
27
+
28
+ class TestEnv(OpenAIMixin):
29
+ def as_tools(self) -> list[mcp_types.Tool]:
30
+ return [
31
+ create_mock_tool(
32
+ "navigate",
33
+ "Navigate to URL",
34
+ {
35
+ "type": "object",
36
+ "properties": {"url": {"type": "string"}},
37
+ "required": ["url"],
38
+ },
39
+ ),
40
+ ]
41
+
42
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
43
+ pass
44
+
45
+ env = TestEnv()
46
+ tools = env.as_openai_chat_tools()
47
+
48
+ assert len(tools) == 1
49
+ assert tools[0]["type"] == "function"
50
+ assert tools[0]["function"]["name"] == "navigate" # type: ignore[typeddict-item]
51
+ assert tools[0]["function"]["description"] == "Navigate to URL" # type: ignore[typeddict-item]
52
+ assert "url" in tools[0]["function"]["parameters"]["properties"] # type: ignore[typeddict-item, operator]
53
+
54
+ def test_as_openai_chat_tools_strict_mode(self) -> None:
55
+ """as_openai_chat_tools with strict=True adds strict flag."""
56
+ from hud.environment.integrations.openai import OpenAIMixin
57
+
58
+ class TestEnv(OpenAIMixin):
59
+ def as_tools(self) -> list[mcp_types.Tool]:
60
+ return [create_mock_tool("test_tool")]
61
+
62
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
63
+ pass
64
+
65
+ env = TestEnv()
66
+ tools = env.as_openai_chat_tools(strict=True)
67
+
68
+ assert tools[0]["function"]["strict"] is True # type: ignore[typeddict-item]
69
+
70
+ def test_as_openai_chat_tools_empty(self) -> None:
71
+ """as_openai_chat_tools returns empty list when no tools."""
72
+ from hud.environment.integrations.openai import OpenAIMixin
73
+
74
+ class TestEnv(OpenAIMixin):
75
+ def as_tools(self) -> list[mcp_types.Tool]:
76
+ return []
77
+
78
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
79
+ pass
80
+
81
+ env = TestEnv()
82
+ tools = env.as_openai_chat_tools()
83
+
84
+ assert tools == []
85
+
86
+ def test_as_openai_responses_tools(self) -> None:
87
+ """as_openai_responses_tools converts to Responses API format."""
88
+ from hud.environment.integrations.openai import OpenAIMixin
89
+
90
+ class TestEnv(OpenAIMixin):
91
+ def as_tools(self) -> list[mcp_types.Tool]:
92
+ return [create_mock_tool("search", "Search the web")]
93
+
94
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
95
+ pass
96
+
97
+ env = TestEnv()
98
+ tools = env.as_openai_responses_tools()
99
+
100
+ assert len(tools) == 1
101
+ assert tools[0]["type"] == "function"
102
+ assert tools[0]["name"] == "search"
103
+ assert tools[0]["description"] == "Search the web"
104
+
105
+
106
+ class TestAnthropicMixin:
107
+ """Tests for Anthropic/Claude format conversion."""
108
+
109
+ def test_as_claude_tools_basic(self) -> None:
110
+ """as_claude_tools converts MCP tools to Claude format."""
111
+ from hud.environment.integrations.anthropic import AnthropicMixin
112
+
113
+ class TestEnv(AnthropicMixin):
114
+ def as_tools(self) -> list[mcp_types.Tool]:
115
+ return [
116
+ create_mock_tool(
117
+ "click",
118
+ "Click element",
119
+ {
120
+ "type": "object",
121
+ "properties": {"selector": {"type": "string"}},
122
+ },
123
+ ),
124
+ ]
125
+
126
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
127
+ pass
128
+
129
+ env = TestEnv()
130
+ tools = env.as_claude_tools()
131
+
132
+ assert len(tools) == 1
133
+ assert tools[0]["name"] == "click"
134
+ assert tools[0]["description"] == "Click element"
135
+ assert "input_schema" in tools[0]
136
+ assert "cache_control" not in tools[0]
137
+
138
+ def test_as_claude_tools_with_cache_control(self) -> None:
139
+ """as_claude_tools with cache_control=True adds cache field."""
140
+ from hud.environment.integrations.anthropic import AnthropicMixin
141
+
142
+ class TestEnv(AnthropicMixin):
143
+ def as_tools(self) -> list[mcp_types.Tool]:
144
+ return [create_mock_tool("test")]
145
+
146
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
147
+ pass
148
+
149
+ env = TestEnv()
150
+ tools = env.as_claude_tools(cache_control=True)
151
+
152
+ assert tools[0]["cache_control"] == {"type": "ephemeral"}
153
+
154
+ def test_as_claude_programmatic_tools(self) -> None:
155
+ """as_claude_programmatic_tools includes allowed_callers."""
156
+ from hud.environment.integrations.anthropic import AnthropicMixin
157
+
158
+ class TestEnv(AnthropicMixin):
159
+ def as_tools(self) -> list[mcp_types.Tool]:
160
+ return [create_mock_tool("analyze")]
161
+
162
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
163
+ pass
164
+
165
+ env = TestEnv()
166
+ tools = env.as_claude_programmatic_tools()
167
+
168
+ assert tools[0]["allowed_callers"] == ["code_execution_20250825"]
169
+
170
+
171
+ class TestGeminiMixin:
172
+ """Tests for Google/Gemini format conversion."""
173
+
174
+ def test_as_gemini_tools_basic(self) -> None:
175
+ """as_gemini_tools converts MCP tools to Gemini format."""
176
+ from hud.environment.integrations.gemini import GeminiMixin
177
+
178
+ class TestEnv(GeminiMixin):
179
+ def as_tools(self) -> list[mcp_types.Tool]:
180
+ return [
181
+ create_mock_tool(
182
+ "search",
183
+ "Search query",
184
+ {
185
+ "type": "object",
186
+ "properties": {"query": {"type": "string"}},
187
+ },
188
+ ),
189
+ ]
190
+
191
+ env = TestEnv()
192
+ tools = env.as_gemini_tools()
193
+
194
+ assert len(tools) == 1
195
+ assert "function_declarations" in tools[0]
196
+ declarations = tools[0]["function_declarations"]
197
+ assert len(declarations) == 1
198
+ assert declarations[0]["name"] == "search"
199
+ assert declarations[0]["description"] == "Search query"
200
+
201
+ def test_as_gemini_tools_multiple(self) -> None:
202
+ """as_gemini_tools wraps multiple tools in single declaration list."""
203
+ from hud.environment.integrations.gemini import GeminiMixin
204
+
205
+ class TestEnv(GeminiMixin):
206
+ def as_tools(self) -> list[mcp_types.Tool]:
207
+ return [
208
+ create_mock_tool("tool1"),
209
+ create_mock_tool("tool2"),
210
+ create_mock_tool("tool3"),
211
+ ]
212
+
213
+ env = TestEnv()
214
+ tools = env.as_gemini_tools()
215
+
216
+ assert len(tools) == 1 # Single wrapper object
217
+ assert len(tools[0]["function_declarations"]) == 3
218
+
219
+ def test_as_gemini_tool_config_auto(self) -> None:
220
+ """as_gemini_tool_config with AUTO mode."""
221
+ from hud.environment.integrations.gemini import GeminiMixin
222
+
223
+ class TestEnv(GeminiMixin):
224
+ def as_tools(self) -> list[mcp_types.Tool]:
225
+ return []
226
+
227
+ env = TestEnv()
228
+ config = env.as_gemini_tool_config(mode="AUTO")
229
+
230
+ assert config["function_calling_config"]["mode"] == "AUTO"
231
+
232
+ def test_as_gemini_tool_config_any_with_allowed(self) -> None:
233
+ """as_gemini_tool_config with ANY mode and allowed tools."""
234
+ from hud.environment.integrations.gemini import GeminiMixin
235
+
236
+ class TestEnv(GeminiMixin):
237
+ def as_tools(self) -> list[mcp_types.Tool]:
238
+ return []
239
+
240
+ env = TestEnv()
241
+ config = env.as_gemini_tool_config(mode="ANY", allowed_tools=["search", "navigate"])
242
+
243
+ assert config["function_calling_config"]["mode"] == "ANY"
244
+ assert config["function_calling_config"]["allowed_function_names"] == ["search", "navigate"]
245
+
246
+ def test_as_gemini_tool_config_none(self) -> None:
247
+ """as_gemini_tool_config with NONE mode disables tools."""
248
+ from hud.environment.integrations.gemini import GeminiMixin
249
+
250
+ class TestEnv(GeminiMixin):
251
+ def as_tools(self) -> list[mcp_types.Tool]:
252
+ return []
253
+
254
+ env = TestEnv()
255
+ config = env.as_gemini_tool_config(mode="NONE")
256
+
257
+ assert config["function_calling_config"]["mode"] == "NONE"
@@ -0,0 +1,242 @@
1
+ """Tests for local connectors - connect_image, connect_server, connect_fastapi."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ from hud.environment.connection import ConnectionType, Connector
9
+
10
+
11
+ class TestConnectImage:
12
+ """Tests for LocalConnectorMixin.connect_image."""
13
+
14
+ def test_connect_image_creates_local_connection(self) -> None:
15
+ """connect_image creates LOCAL connection with docker command."""
16
+ from hud.environment.connectors.local import LocalConnectorMixin
17
+
18
+ class TestEnv(LocalConnectorMixin):
19
+ def __init__(self) -> None:
20
+ self._connections: dict[str, Connector] = {}
21
+
22
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
23
+ pass
24
+
25
+ # Mock the import that happens inside connect_image
26
+ mock_docker_utils = MagicMock()
27
+ mock_docker_utils.create_docker_run_command.return_value = [
28
+ "docker",
29
+ "run",
30
+ "-i",
31
+ "--rm",
32
+ "mcp/fetch",
33
+ ]
34
+
35
+ with patch.dict(
36
+ "sys.modules",
37
+ {"hud.cli.utils.docker": mock_docker_utils},
38
+ ):
39
+ env = TestEnv()
40
+ env.connect_image("mcp/fetch")
41
+
42
+ assert "mcp/fetch" in env._connections
43
+ conn = env._connections["mcp/fetch"]
44
+ assert conn.connection_type == ConnectionType.LOCAL
45
+ mock_docker_utils.create_docker_run_command.assert_called_once()
46
+
47
+ def test_connect_image_with_alias(self) -> None:
48
+ """connect_image uses alias for connection name."""
49
+ from hud.environment.connectors.local import LocalConnectorMixin
50
+
51
+ class TestEnv(LocalConnectorMixin):
52
+ def __init__(self) -> None:
53
+ self._connections: dict[str, Connector] = {}
54
+
55
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
56
+ pass
57
+
58
+ mock_docker_utils = MagicMock()
59
+ mock_docker_utils.create_docker_run_command.return_value = [
60
+ "docker",
61
+ "run",
62
+ "-i",
63
+ "--rm",
64
+ "mcp/fetch",
65
+ ]
66
+
67
+ with patch.dict(
68
+ "sys.modules",
69
+ {"hud.cli.utils.docker": mock_docker_utils},
70
+ ):
71
+ env = TestEnv()
72
+ env.connect_image("mcp/fetch", alias="fetcher")
73
+
74
+ assert "fetcher" in env._connections
75
+ assert "mcp/fetch" not in env._connections
76
+
77
+ def test_connect_image_with_prefix(self) -> None:
78
+ """connect_image passes prefix to config."""
79
+ from hud.environment.connectors.local import LocalConnectorMixin
80
+
81
+ class TestEnv(LocalConnectorMixin):
82
+ def __init__(self) -> None:
83
+ self._connections: dict[str, Connector] = {}
84
+
85
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
86
+ pass
87
+
88
+ mock_docker_utils = MagicMock()
89
+ mock_docker_utils.create_docker_run_command.return_value = [
90
+ "docker",
91
+ "run",
92
+ "-i",
93
+ "--rm",
94
+ "mcp/fetch",
95
+ ]
96
+
97
+ with patch.dict(
98
+ "sys.modules",
99
+ {"hud.cli.utils.docker": mock_docker_utils},
100
+ ):
101
+ env = TestEnv()
102
+ env.connect_image("mcp/fetch", prefix="fetch")
103
+
104
+ conn = env._connections["mcp/fetch"]
105
+ assert conn.config.prefix == "fetch"
106
+
107
+ def test_connect_image_returns_self(self) -> None:
108
+ """connect_image returns self for chaining."""
109
+ from hud.environment.connectors.local import LocalConnectorMixin
110
+
111
+ class TestEnv(LocalConnectorMixin):
112
+ def __init__(self) -> None:
113
+ self._connections: dict[str, Connector] = {}
114
+
115
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
116
+ pass
117
+
118
+ mock_docker_utils = MagicMock()
119
+ mock_docker_utils.create_docker_run_command.return_value = [
120
+ "docker",
121
+ "run",
122
+ "-i",
123
+ "--rm",
124
+ "mcp/fetch",
125
+ ]
126
+
127
+ with patch.dict(
128
+ "sys.modules",
129
+ {"hud.cli.utils.docker": mock_docker_utils},
130
+ ):
131
+ env = TestEnv()
132
+ result = env.connect_image("mcp/fetch")
133
+
134
+ assert result is env
135
+
136
+
137
+ class TestConnectServer:
138
+ """Tests for LocalConnectorMixin.connect_server."""
139
+
140
+ def test_connect_server_calls_include_router(self) -> None:
141
+ """connect_server calls include_router with server and prefix."""
142
+ from hud.environment.connectors.local import LocalConnectorMixin
143
+
144
+ class TestEnv(LocalConnectorMixin):
145
+ def __init__(self) -> None:
146
+ self._connections: dict[str, Connector] = {}
147
+ self.routers: list[tuple[Any, str | None]] = []
148
+
149
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
150
+ self.routers.append((server, prefix))
151
+
152
+ env = TestEnv()
153
+ mock_server = MagicMock()
154
+ env.connect_server(mock_server, prefix="tools")
155
+
156
+ assert len(env.routers) == 1
157
+ assert env.routers[0] == (mock_server, "tools")
158
+
159
+ def test_connect_server_returns_self(self) -> None:
160
+ """connect_server returns self for chaining."""
161
+ from hud.environment.connectors.local import LocalConnectorMixin
162
+
163
+ class TestEnv(LocalConnectorMixin):
164
+ def __init__(self) -> None:
165
+ self._connections: dict[str, Connector] = {}
166
+
167
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
168
+ pass
169
+
170
+ env = TestEnv()
171
+ result = env.connect_server(MagicMock())
172
+
173
+ assert result is env
174
+
175
+
176
+ class TestConnectFastAPI:
177
+ """Tests for LocalConnectorMixin.connect_fastapi."""
178
+
179
+ @patch("fastmcp.FastMCP")
180
+ def test_connect_fastapi_creates_mcp_server(self, mock_fastmcp: MagicMock) -> None:
181
+ """connect_fastapi converts FastAPI app to MCP server."""
182
+ from hud.environment.connectors.local import LocalConnectorMixin
183
+
184
+ mock_mcp_server = MagicMock()
185
+ mock_fastmcp.from_fastapi.return_value = mock_mcp_server
186
+
187
+ class TestEnv(LocalConnectorMixin):
188
+ def __init__(self) -> None:
189
+ self._connections: dict[str, Connector] = {}
190
+ self.routers: list[tuple[Any, str | None]] = []
191
+
192
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
193
+ self.routers.append((server, prefix))
194
+
195
+ env = TestEnv()
196
+ mock_app = MagicMock()
197
+ mock_app.title = "My API"
198
+ env.connect_fastapi(mock_app)
199
+
200
+ mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="My API")
201
+ assert len(env.routers) == 1
202
+ assert env.routers[0] == (mock_mcp_server, None)
203
+
204
+ @patch("fastmcp.FastMCP")
205
+ def test_connect_fastapi_with_custom_name(self, mock_fastmcp: MagicMock) -> None:
206
+ """connect_fastapi uses custom name if provided."""
207
+ from hud.environment.connectors.local import LocalConnectorMixin
208
+
209
+ mock_fastmcp.from_fastapi.return_value = MagicMock()
210
+
211
+ class TestEnv(LocalConnectorMixin):
212
+ def __init__(self) -> None:
213
+ self._connections: dict[str, Connector] = {}
214
+
215
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
216
+ pass
217
+
218
+ env = TestEnv()
219
+ mock_app = MagicMock()
220
+ mock_app.title = "Original"
221
+ env.connect_fastapi(mock_app, name="custom-api")
222
+
223
+ mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="custom-api")
224
+
225
+ @patch("fastmcp.FastMCP")
226
+ def test_connect_fastapi_returns_self(self, mock_fastmcp: MagicMock) -> None:
227
+ """connect_fastapi returns self for chaining."""
228
+ from hud.environment.connectors.local import LocalConnectorMixin
229
+
230
+ mock_fastmcp.from_fastapi.return_value = MagicMock()
231
+
232
+ class TestEnv(LocalConnectorMixin):
233
+ def __init__(self) -> None:
234
+ self._connections: dict[str, Connector] = {}
235
+
236
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
237
+ pass
238
+
239
+ env = TestEnv()
240
+ result = env.connect_fastapi(MagicMock())
241
+
242
+ assert result is env