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,317 @@
1
+ """Tests for hud.environment.connection module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from unittest.mock import AsyncMock, MagicMock, patch
6
+
7
+ import mcp.types as mcp_types
8
+ import pytest
9
+
10
+ from hud.environment.connection import ConnectionConfig, ConnectionType, Connector
11
+
12
+
13
+ class TestConnectionConfig:
14
+ """Tests for ConnectionConfig."""
15
+
16
+ def test_default_config(self) -> None:
17
+ """Config with no options set."""
18
+ config = ConnectionConfig()
19
+ assert config.prefix is None
20
+ assert config.include is None
21
+ assert config.exclude is None
22
+ assert config.transform is None
23
+
24
+ def test_config_with_options(self) -> None:
25
+ """Config with all options set."""
26
+ transform_fn = lambda t: t # noqa: E731
27
+ config = ConnectionConfig(
28
+ prefix="test",
29
+ include=["tool1", "tool2"],
30
+ exclude=["tool3"],
31
+ transform=transform_fn,
32
+ )
33
+ assert config.prefix == "test"
34
+ assert config.include == ["tool1", "tool2"]
35
+ assert config.exclude == ["tool3"]
36
+ assert config.transform is transform_fn
37
+
38
+
39
+ class TestConnectionType:
40
+ """Tests for ConnectionType enum."""
41
+
42
+ def test_local_type(self) -> None:
43
+ """LOCAL type for stdio/Docker connections."""
44
+ assert ConnectionType.LOCAL.value == "local"
45
+
46
+ def test_remote_type(self) -> None:
47
+ """REMOTE type for HTTP connections."""
48
+ assert ConnectionType.REMOTE.value == "remote"
49
+
50
+
51
+ class TestConnector:
52
+ """Tests for Connector class."""
53
+
54
+ def test_init_stores_transport_config(self) -> None:
55
+ """__init__ stores transport config, doesn't create client."""
56
+ transport = {"server": {"url": "http://example.com"}}
57
+ config = ConnectionConfig()
58
+
59
+ connector = Connector(
60
+ transport=transport,
61
+ config=config,
62
+ name="test",
63
+ connection_type=ConnectionType.REMOTE,
64
+ auth="test-token",
65
+ )
66
+
67
+ assert connector._transport == transport
68
+ assert connector._auth == "test-token"
69
+ assert connector.name == "test"
70
+ assert connector.connection_type == ConnectionType.REMOTE
71
+ assert connector.client is None # Not created yet
72
+ assert connector._tools_cache is None
73
+
74
+ def test_is_local_property(self) -> None:
75
+ """is_local returns True for LOCAL connections."""
76
+ connector = Connector(
77
+ transport={},
78
+ config=ConnectionConfig(),
79
+ name="local-test",
80
+ connection_type=ConnectionType.LOCAL,
81
+ )
82
+ assert connector.is_local is True
83
+ assert connector.is_remote is False
84
+
85
+ def test_is_remote_property(self) -> None:
86
+ """is_remote returns True for REMOTE connections."""
87
+ connector = Connector(
88
+ transport={},
89
+ config=ConnectionConfig(),
90
+ name="remote-test",
91
+ connection_type=ConnectionType.REMOTE,
92
+ )
93
+ assert connector.is_remote is True
94
+ assert connector.is_local is False
95
+
96
+ def test_is_connected_false_when_no_client(self) -> None:
97
+ """is_connected returns False when client is None."""
98
+ connector = Connector(
99
+ transport={},
100
+ config=ConnectionConfig(),
101
+ name="test",
102
+ connection_type=ConnectionType.REMOTE,
103
+ )
104
+ assert connector.is_connected is False
105
+
106
+ def test_cached_tools_empty_initially(self) -> None:
107
+ """cached_tools returns empty list initially."""
108
+ connector = Connector(
109
+ transport={},
110
+ config=ConnectionConfig(),
111
+ name="test",
112
+ connection_type=ConnectionType.REMOTE,
113
+ )
114
+ assert connector.cached_tools == []
115
+
116
+ @pytest.mark.asyncio
117
+ async def test_connect_creates_client(self) -> None:
118
+ """connect() creates FastMCPClient and enters context."""
119
+ transport = {"server": {"url": "http://example.com"}}
120
+ connector = Connector(
121
+ transport=transport,
122
+ config=ConnectionConfig(),
123
+ name="test",
124
+ connection_type=ConnectionType.REMOTE,
125
+ auth="test-token",
126
+ )
127
+
128
+ mock_client = MagicMock()
129
+ mock_client.__aenter__ = AsyncMock(return_value=mock_client)
130
+ mock_client.is_connected = MagicMock(return_value=True)
131
+
132
+ # Patch where it's imported from, not where it's used
133
+ with patch("fastmcp.client.Client", return_value=mock_client) as mock_cls:
134
+ await connector.connect()
135
+
136
+ # Client was created with correct args
137
+ mock_cls.assert_called_once_with(transport=transport, auth="test-token")
138
+ # Client context was entered
139
+ mock_client.__aenter__.assert_called_once()
140
+ # Client is now set
141
+ assert connector.client is mock_client
142
+
143
+ @pytest.mark.asyncio
144
+ async def test_disconnect_clears_client(self) -> None:
145
+ """disconnect() exits client context and clears state."""
146
+ connector = Connector(
147
+ transport={},
148
+ config=ConnectionConfig(),
149
+ name="test",
150
+ connection_type=ConnectionType.REMOTE,
151
+ )
152
+
153
+ mock_client = MagicMock()
154
+ mock_client.__aexit__ = AsyncMock(return_value=None)
155
+ mock_client.is_connected = MagicMock(return_value=True)
156
+ connector.client = mock_client
157
+ connector._tools_cache = [MagicMock()]
158
+
159
+ await connector.disconnect()
160
+
161
+ mock_client.__aexit__.assert_called_once_with(None, None, None)
162
+ assert connector.client is None
163
+ assert connector._tools_cache is None
164
+
165
+ @pytest.mark.asyncio
166
+ async def test_list_tools_raises_when_not_connected(self) -> None:
167
+ """list_tools() raises RuntimeError when not connected."""
168
+ connector = Connector(
169
+ transport={},
170
+ config=ConnectionConfig(),
171
+ name="test",
172
+ connection_type=ConnectionType.REMOTE,
173
+ )
174
+
175
+ with pytest.raises(RuntimeError, match="Not connected"):
176
+ await connector.list_tools()
177
+
178
+ @pytest.mark.asyncio
179
+ async def test_list_tools_applies_include_filter(self) -> None:
180
+ """list_tools() filters tools based on include list."""
181
+ connector = Connector(
182
+ transport={},
183
+ config=ConnectionConfig(include=["tool1"]),
184
+ name="test",
185
+ connection_type=ConnectionType.REMOTE,
186
+ )
187
+
188
+ mock_client = MagicMock()
189
+ mock_client.list_tools = AsyncMock(
190
+ return_value=[
191
+ mcp_types.Tool(name="tool1", description="Tool 1", inputSchema={}),
192
+ mcp_types.Tool(name="tool2", description="Tool 2", inputSchema={}),
193
+ ]
194
+ )
195
+ connector.client = mock_client
196
+
197
+ tools = await connector.list_tools()
198
+
199
+ assert len(tools) == 1
200
+ assert tools[0].name == "tool1"
201
+
202
+ @pytest.mark.asyncio
203
+ async def test_list_tools_applies_exclude_filter(self) -> None:
204
+ """list_tools() filters out tools in exclude list."""
205
+ connector = Connector(
206
+ transport={},
207
+ config=ConnectionConfig(exclude=["tool2"]),
208
+ name="test",
209
+ connection_type=ConnectionType.REMOTE,
210
+ )
211
+
212
+ mock_client = MagicMock()
213
+ mock_client.list_tools = AsyncMock(
214
+ return_value=[
215
+ mcp_types.Tool(name="tool1", description="Tool 1", inputSchema={}),
216
+ mcp_types.Tool(name="tool2", description="Tool 2", inputSchema={}),
217
+ ]
218
+ )
219
+ connector.client = mock_client
220
+
221
+ tools = await connector.list_tools()
222
+
223
+ assert len(tools) == 1
224
+ assert tools[0].name == "tool1"
225
+
226
+ @pytest.mark.asyncio
227
+ async def test_list_tools_applies_prefix(self) -> None:
228
+ """list_tools() adds prefix to tool names."""
229
+ connector = Connector(
230
+ transport={},
231
+ config=ConnectionConfig(prefix="myprefix"),
232
+ name="test",
233
+ connection_type=ConnectionType.REMOTE,
234
+ )
235
+
236
+ mock_client = MagicMock()
237
+ mock_client.list_tools = AsyncMock(
238
+ return_value=[
239
+ mcp_types.Tool(name="tool1", description="Tool 1", inputSchema={}),
240
+ ]
241
+ )
242
+ connector.client = mock_client
243
+
244
+ tools = await connector.list_tools()
245
+
246
+ assert len(tools) == 1
247
+ assert tools[0].name == "myprefix_tool1"
248
+
249
+ @pytest.mark.asyncio
250
+ async def test_list_tools_caches_results(self) -> None:
251
+ """list_tools() caches results."""
252
+ connector = Connector(
253
+ transport={},
254
+ config=ConnectionConfig(),
255
+ name="test",
256
+ connection_type=ConnectionType.REMOTE,
257
+ )
258
+
259
+ mock_client = MagicMock()
260
+ mock_client.list_tools = AsyncMock(
261
+ return_value=[
262
+ mcp_types.Tool(name="tool1", description="Tool 1", inputSchema={}),
263
+ ]
264
+ )
265
+ connector.client = mock_client
266
+
267
+ tools = await connector.list_tools()
268
+
269
+ assert connector._tools_cache == tools
270
+ assert connector.cached_tools == tools
271
+
272
+ @pytest.mark.asyncio
273
+ async def test_call_tool_strips_prefix(self) -> None:
274
+ """call_tool() strips prefix before calling."""
275
+ connector = Connector(
276
+ transport={},
277
+ config=ConnectionConfig(prefix="myprefix"),
278
+ name="test",
279
+ connection_type=ConnectionType.REMOTE,
280
+ )
281
+
282
+ mock_result = mcp_types.CallToolResult(content=[], isError=False)
283
+ mock_client = MagicMock()
284
+ mock_client.call_tool_mcp = AsyncMock(return_value=mock_result)
285
+ connector.client = mock_client
286
+
287
+ await connector.call_tool("myprefix_tool1", {"arg": "value"})
288
+
289
+ # Prefix should be stripped
290
+ mock_client.call_tool_mcp.assert_called_once_with("tool1", {"arg": "value"})
291
+
292
+ @pytest.mark.asyncio
293
+ async def test_call_tool_raises_when_not_connected(self) -> None:
294
+ """call_tool() raises RuntimeError when not connected."""
295
+ connector = Connector(
296
+ transport={},
297
+ config=ConnectionConfig(),
298
+ name="test",
299
+ connection_type=ConnectionType.REMOTE,
300
+ )
301
+
302
+ with pytest.raises(RuntimeError, match="Not connected"):
303
+ await connector.call_tool("tool1", {})
304
+
305
+ def test_repr(self) -> None:
306
+ """__repr__ shows useful info."""
307
+ connector = Connector(
308
+ transport={},
309
+ config=ConnectionConfig(),
310
+ name="my-server",
311
+ connection_type=ConnectionType.REMOTE,
312
+ )
313
+
314
+ repr_str = repr(connector)
315
+ assert "my-server" in repr_str
316
+ assert "remote" in repr_str
317
+ assert "connected=False" in repr_str
@@ -0,0 +1,205 @@
1
+ """Tests for hud.environment.connectors module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from unittest.mock import patch
7
+
8
+ from hud.environment.connection import ConnectionType, Connector
9
+
10
+
11
+ class TestBaseConnectorMixin:
12
+ """Tests for BaseConnectorMixin._add_connection."""
13
+
14
+ def test_add_connection_stores_transport_config(self) -> None:
15
+ """_add_connection stores transport, doesn't create client."""
16
+ from hud.environment.connectors.base import BaseConnectorMixin
17
+
18
+ class TestEnv(BaseConnectorMixin):
19
+ def __init__(self) -> None:
20
+ self._connections: dict[str, Connector] = {}
21
+
22
+ env = TestEnv()
23
+ transport = {"server": {"url": "http://example.com"}}
24
+
25
+ env._add_connection(
26
+ "test-server",
27
+ transport,
28
+ connection_type=ConnectionType.REMOTE,
29
+ auth="test-token",
30
+ prefix="myprefix",
31
+ )
32
+
33
+ assert "test-server" in env._connections
34
+ conn = env._connections["test-server"]
35
+ assert conn._transport == transport
36
+ assert conn._auth == "test-token"
37
+ assert conn.config.prefix == "myprefix"
38
+ assert conn.client is None # Not created yet
39
+
40
+ def test_add_connection_returns_self(self) -> None:
41
+ """_add_connection returns self for chaining."""
42
+ from hud.environment.connectors.base import BaseConnectorMixin
43
+
44
+ class TestEnv(BaseConnectorMixin):
45
+ def __init__(self) -> None:
46
+ self._connections: dict[str, Connector] = {}
47
+
48
+ env = TestEnv()
49
+ result = env._add_connection(
50
+ "test",
51
+ {},
52
+ connection_type=ConnectionType.REMOTE,
53
+ )
54
+
55
+ assert result is env
56
+
57
+
58
+ class TestMCPConfigConnectorMixin:
59
+ """Tests for MCPConfigConnectorMixin."""
60
+
61
+ def test_connect_mcp_detects_local_connection(self) -> None:
62
+ """connect_mcp detects LOCAL type from command in config."""
63
+ from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
64
+
65
+ class TestEnv(MCPConfigConnectorMixin):
66
+ def __init__(self) -> None:
67
+ self._connections: dict[str, Connector] = {}
68
+
69
+ env = TestEnv()
70
+ config = {
71
+ "filesystem": {
72
+ "command": "npx",
73
+ "args": ["-y", "@modelcontextprotocol/server-filesystem"],
74
+ }
75
+ }
76
+
77
+ env.connect_mcp(config)
78
+
79
+ conn = env._connections["filesystem"]
80
+ assert conn.connection_type == ConnectionType.LOCAL
81
+
82
+ def test_connect_mcp_detects_remote_connection(self) -> None:
83
+ """connect_mcp detects REMOTE type from URL in config."""
84
+ from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
85
+
86
+ class TestEnv(MCPConfigConnectorMixin):
87
+ def __init__(self) -> None:
88
+ self._connections: dict[str, Connector] = {}
89
+
90
+ env = TestEnv()
91
+ config = {
92
+ "browser": {
93
+ "url": "https://mcp.hud.ai/browser",
94
+ }
95
+ }
96
+
97
+ env.connect_mcp(config)
98
+
99
+ conn = env._connections["browser"]
100
+ assert conn.connection_type == ConnectionType.REMOTE
101
+
102
+ def test_connect_mcp_uses_alias(self) -> None:
103
+ """connect_mcp uses alias if provided."""
104
+ from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
105
+
106
+ class TestEnv(MCPConfigConnectorMixin):
107
+ def __init__(self) -> None:
108
+ self._connections: dict[str, Connector] = {}
109
+
110
+ env = TestEnv()
111
+ config = {"server": {"url": "http://example.com"}}
112
+
113
+ env.connect_mcp(config, alias="my-alias")
114
+
115
+ assert "my-alias" in env._connections
116
+ assert "server" not in env._connections
117
+
118
+ def test_connect_mcp_config_creates_multiple_connections(self) -> None:
119
+ """connect_mcp_config creates a connection for each server."""
120
+ from hud.environment.connectors.mcp_config import MCPConfigConnectorMixin
121
+
122
+ class TestEnv(MCPConfigConnectorMixin):
123
+ def __init__(self) -> None:
124
+ self._connections: dict[str, Connector] = {}
125
+
126
+ env = TestEnv()
127
+ mcp_config = {
128
+ "server1": {"url": "http://example1.com"},
129
+ "server2": {"url": "http://example2.com"},
130
+ "server3": {"command": "npx", "args": ["server"]},
131
+ }
132
+
133
+ env.connect_mcp_config(mcp_config)
134
+
135
+ assert len(env._connections) == 3
136
+ assert "server1" in env._connections
137
+ assert "server2" in env._connections
138
+ assert "server3" in env._connections
139
+
140
+
141
+ class TestRemoteConnectorMixin:
142
+ """Tests for RemoteConnectorMixin."""
143
+
144
+ def test_connect_url_creates_remote_connection(self) -> None:
145
+ """connect_url creates REMOTE connection."""
146
+ from hud.environment.connectors.remote import RemoteConnectorMixin
147
+
148
+ class TestEnv(RemoteConnectorMixin):
149
+ def __init__(self) -> None:
150
+ self._connections: dict[str, Connector] = {}
151
+
152
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
153
+ pass
154
+
155
+ env = TestEnv()
156
+ env.connect_url("https://mcp.example.com", alias="example")
157
+
158
+ assert "example" in env._connections
159
+ conn = env._connections["example"]
160
+ assert conn.connection_type == ConnectionType.REMOTE
161
+
162
+ def test_connect_url_extracts_auth_from_headers(self) -> None:
163
+ """connect_url extracts Authorization from headers."""
164
+ from hud.environment.connectors.remote import RemoteConnectorMixin
165
+
166
+ class TestEnv(RemoteConnectorMixin):
167
+ def __init__(self) -> None:
168
+ self._connections: dict[str, Connector] = {}
169
+
170
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
171
+ pass
172
+
173
+ env = TestEnv()
174
+ env.connect_url(
175
+ "https://mcp.example.com",
176
+ headers={"Authorization": "Bearer my-token"},
177
+ alias="example",
178
+ )
179
+
180
+ conn = env._connections["example"]
181
+ assert conn._auth == "Bearer my-token"
182
+
183
+ def test_connect_hub_creates_connection(self) -> None:
184
+ """connect_hub creates connection with correct config."""
185
+ from hud.environment.connectors.remote import RemoteConnectorMixin
186
+
187
+ class TestEnv(RemoteConnectorMixin):
188
+ def __init__(self) -> None:
189
+ self._connections: dict[str, Connector] = {}
190
+ self._hub_config: dict[str, Any] | None = None
191
+
192
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
193
+ pass
194
+
195
+ env = TestEnv()
196
+ with patch("hud.settings.settings") as mock_settings:
197
+ mock_settings.hud_mcp_url = "https://mcp.hud.ai"
198
+ mock_settings.client_timeout = 300 # Used in connect_mcp for sse_read_timeout
199
+
200
+ env.connect_hub("browser")
201
+
202
+ # connect_hub creates a connection named "hud" (from mcp_config key)
203
+ assert "hud" in env._connections
204
+ # Verify hub config is stored for serialization
205
+ assert env._hub_config == {"name": "browser"}