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
@@ -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,201 @@
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
+ @patch("hud.cli.utils.docker.create_docker_run_command")
15
+ def test_connect_image_creates_local_connection(self, mock_docker_cmd: MagicMock) -> None:
16
+ """connect_image creates LOCAL connection with docker command."""
17
+ from hud.environment.connectors.local import LocalConnectorMixin
18
+
19
+ mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
20
+
21
+ class TestEnv(LocalConnectorMixin):
22
+ def __init__(self) -> None:
23
+ self._connections: dict[str, Connector] = {}
24
+
25
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
26
+ pass
27
+
28
+ env = TestEnv()
29
+ env.connect_image("mcp/fetch")
30
+
31
+ assert "mcp/fetch" in env._connections
32
+ conn = env._connections["mcp/fetch"]
33
+ assert conn.connection_type == ConnectionType.LOCAL
34
+ mock_docker_cmd.assert_called_once()
35
+
36
+ @patch("hud.cli.utils.docker.create_docker_run_command")
37
+ def test_connect_image_with_alias(self, mock_docker_cmd: MagicMock) -> None:
38
+ """connect_image uses alias for connection name."""
39
+ from hud.environment.connectors.local import LocalConnectorMixin
40
+
41
+ mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
42
+
43
+ class TestEnv(LocalConnectorMixin):
44
+ def __init__(self) -> None:
45
+ self._connections: dict[str, Connector] = {}
46
+
47
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
48
+ pass
49
+
50
+ env = TestEnv()
51
+ env.connect_image("mcp/fetch", alias="fetcher")
52
+
53
+ assert "fetcher" in env._connections
54
+ assert "mcp/fetch" not in env._connections
55
+
56
+ @patch("hud.cli.utils.docker.create_docker_run_command")
57
+ def test_connect_image_with_prefix(self, mock_docker_cmd: MagicMock) -> None:
58
+ """connect_image passes prefix to config."""
59
+ from hud.environment.connectors.local import LocalConnectorMixin
60
+
61
+ mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
62
+
63
+ class TestEnv(LocalConnectorMixin):
64
+ def __init__(self) -> None:
65
+ self._connections: dict[str, Connector] = {}
66
+
67
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
68
+ pass
69
+
70
+ env = TestEnv()
71
+ env.connect_image("mcp/fetch", prefix="fetch")
72
+
73
+ conn = env._connections["mcp/fetch"]
74
+ assert conn.config.prefix == "fetch"
75
+
76
+ @patch("hud.cli.utils.docker.create_docker_run_command")
77
+ def test_connect_image_returns_self(self, mock_docker_cmd: MagicMock) -> None:
78
+ """connect_image returns self for chaining."""
79
+ from hud.environment.connectors.local import LocalConnectorMixin
80
+
81
+ mock_docker_cmd.return_value = ["docker", "run", "-i", "--rm", "mcp/fetch"]
82
+
83
+ class TestEnv(LocalConnectorMixin):
84
+ def __init__(self) -> None:
85
+ self._connections: dict[str, Connector] = {}
86
+
87
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
88
+ pass
89
+
90
+ env = TestEnv()
91
+ result = env.connect_image("mcp/fetch")
92
+
93
+ assert result is env
94
+
95
+
96
+ class TestConnectServer:
97
+ """Tests for LocalConnectorMixin.connect_server."""
98
+
99
+ def test_connect_server_calls_include_router(self) -> None:
100
+ """connect_server calls include_router with server and prefix."""
101
+ from hud.environment.connectors.local import LocalConnectorMixin
102
+
103
+ class TestEnv(LocalConnectorMixin):
104
+ def __init__(self) -> None:
105
+ self._connections: dict[str, Connector] = {}
106
+ self.routers: list[tuple[Any, str | None]] = []
107
+
108
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
109
+ self.routers.append((server, prefix))
110
+
111
+ env = TestEnv()
112
+ mock_server = MagicMock()
113
+ env.connect_server(mock_server, prefix="tools")
114
+
115
+ assert len(env.routers) == 1
116
+ assert env.routers[0] == (mock_server, "tools")
117
+
118
+ def test_connect_server_returns_self(self) -> None:
119
+ """connect_server returns self for chaining."""
120
+ from hud.environment.connectors.local import LocalConnectorMixin
121
+
122
+ class TestEnv(LocalConnectorMixin):
123
+ def __init__(self) -> None:
124
+ self._connections: dict[str, Connector] = {}
125
+
126
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
127
+ pass
128
+
129
+ env = TestEnv()
130
+ result = env.connect_server(MagicMock())
131
+
132
+ assert result is env
133
+
134
+
135
+ class TestConnectFastAPI:
136
+ """Tests for LocalConnectorMixin.connect_fastapi."""
137
+
138
+ @patch("fastmcp.FastMCP")
139
+ def test_connect_fastapi_creates_mcp_server(self, mock_fastmcp: MagicMock) -> None:
140
+ """connect_fastapi converts FastAPI app to MCP server."""
141
+ from hud.environment.connectors.local import LocalConnectorMixin
142
+
143
+ mock_mcp_server = MagicMock()
144
+ mock_fastmcp.from_fastapi.return_value = mock_mcp_server
145
+
146
+ class TestEnv(LocalConnectorMixin):
147
+ def __init__(self) -> None:
148
+ self._connections: dict[str, Connector] = {}
149
+ self.routers: list[tuple[Any, str | None]] = []
150
+
151
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
152
+ self.routers.append((server, prefix))
153
+
154
+ env = TestEnv()
155
+ mock_app = MagicMock()
156
+ mock_app.title = "My API"
157
+ env.connect_fastapi(mock_app)
158
+
159
+ mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="My API")
160
+ assert len(env.routers) == 1
161
+ assert env.routers[0] == (mock_mcp_server, None)
162
+
163
+ @patch("fastmcp.FastMCP")
164
+ def test_connect_fastapi_with_custom_name(self, mock_fastmcp: MagicMock) -> None:
165
+ """connect_fastapi uses custom name if provided."""
166
+ from hud.environment.connectors.local import LocalConnectorMixin
167
+
168
+ mock_fastmcp.from_fastapi.return_value = MagicMock()
169
+
170
+ class TestEnv(LocalConnectorMixin):
171
+ def __init__(self) -> None:
172
+ self._connections: dict[str, Connector] = {}
173
+
174
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
175
+ pass
176
+
177
+ env = TestEnv()
178
+ mock_app = MagicMock()
179
+ mock_app.title = "Original"
180
+ env.connect_fastapi(mock_app, name="custom-api")
181
+
182
+ mock_fastmcp.from_fastapi.assert_called_once_with(app=mock_app, name="custom-api")
183
+
184
+ @patch("fastmcp.FastMCP")
185
+ def test_connect_fastapi_returns_self(self, mock_fastmcp: MagicMock) -> None:
186
+ """connect_fastapi returns self for chaining."""
187
+ from hud.environment.connectors.local import LocalConnectorMixin
188
+
189
+ mock_fastmcp.from_fastapi.return_value = MagicMock()
190
+
191
+ class TestEnv(LocalConnectorMixin):
192
+ def __init__(self) -> None:
193
+ self._connections: dict[str, Connector] = {}
194
+
195
+ def include_router(self, server: Any, *, prefix: str | None = None) -> None:
196
+ pass
197
+
198
+ env = TestEnv()
199
+ result = env.connect_fastapi(MagicMock())
200
+
201
+ assert result is env