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,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,218 @@
1
+ """Tests for hud.environment.connectors module."""
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 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
+ @patch("httpx.Client")
184
+ def test_connect_hub_fetches_config(self, mock_httpx_cls: MagicMock) -> None:
185
+ """connect_hub fetches mcp_config from API."""
186
+ from hud.environment.connectors.remote import RemoteConnectorMixin
187
+
188
+ class TestEnv(RemoteConnectorMixin):
189
+ def __init__(self) -> None:
190
+ self._connections: dict[str, Connector] = {}
191
+
192
+ def mount(self, server: Any, *, prefix: str | None = None) -> None:
193
+ pass
194
+
195
+ # Mock httpx response
196
+ mock_response = MagicMock()
197
+ mock_response.json.return_value = {
198
+ "mcp_config": {
199
+ "browser": {"url": "https://mcp.hud.ai/browser"},
200
+ }
201
+ }
202
+ mock_response.raise_for_status = MagicMock()
203
+
204
+ mock_client = MagicMock()
205
+ mock_client.get.return_value = mock_response
206
+ mock_client.__enter__ = MagicMock(return_value=mock_client)
207
+ mock_client.__exit__ = MagicMock(return_value=None)
208
+ mock_httpx_cls.return_value = mock_client
209
+
210
+ env = TestEnv()
211
+ with patch("hud.settings.settings") as mock_settings:
212
+ mock_settings.hud_api_url = "https://api.hud.so"
213
+ mock_settings.api_key = "test-key"
214
+
215
+ env.connect_hub("hud/browser")
216
+
217
+ # connect_hub creates a connection named "hud" (the server name)
218
+ assert "hud" in env._connections
@@ -0,0 +1,161 @@
1
+ """Tests for Environment class - context manager, resources, prompts, prompt feature."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+
8
+ class TestEnvironmentPrompt:
9
+ """Tests for Environment.prompt feature."""
10
+
11
+ def test_prompt_defaults_to_none(self) -> None:
12
+ """Environment.prompt defaults to None."""
13
+ from hud.environment import Environment
14
+
15
+ env = Environment("test")
16
+ assert env.prompt is None
17
+
18
+ def test_prompt_can_be_set(self) -> None:
19
+ """Environment.prompt can be set manually."""
20
+ from hud.environment import Environment
21
+
22
+ env = Environment("test")
23
+ env.prompt = "Navigate to google.com"
24
+ assert env.prompt == "Navigate to google.com"
25
+
26
+
27
+ class TestEnvironmentContextManager:
28
+ """Tests for Environment async context manager."""
29
+
30
+ @pytest.mark.asyncio
31
+ async def test_context_manager_sets_in_context_flag(self) -> None:
32
+ """Context manager sets _in_context flag."""
33
+ from hud.environment import Environment
34
+
35
+ env = Environment("test")
36
+
37
+ assert env._in_context is False
38
+
39
+ async with env:
40
+ assert env._in_context is True
41
+
42
+ assert env._in_context is False
43
+
44
+ @pytest.mark.asyncio
45
+ async def test_context_manager_no_connections(self) -> None:
46
+ """Context manager works with no connections."""
47
+ from hud.environment import Environment
48
+
49
+ env = Environment("test")
50
+
51
+ async with env:
52
+ # Should work without connections
53
+ pass
54
+
55
+
56
+ class TestEnvironmentResources:
57
+ """Tests for Environment resource operations."""
58
+
59
+ @pytest.mark.asyncio
60
+ async def test_list_resources_empty(self) -> None:
61
+ """list_resources returns empty list when no resources."""
62
+ from hud.environment import Environment
63
+
64
+ env = Environment("test")
65
+
66
+ async with env:
67
+ resources = await env.list_resources()
68
+
69
+ assert resources == []
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_read_resource_not_found(self) -> None:
73
+ """read_resource raises when resource not found."""
74
+ from hud.environment import Environment
75
+
76
+ env = Environment("test")
77
+
78
+ async with env:
79
+ with pytest.raises(ValueError, match="Resource not found"):
80
+ await env.read_resource("file://nonexistent.txt")
81
+
82
+
83
+ class TestEnvironmentPrompts:
84
+ """Tests for Environment prompt operations (MCP prompts, not task prompt)."""
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_list_prompts_empty(self) -> None:
88
+ """list_prompts returns empty list when no prompts."""
89
+ from hud.environment import Environment
90
+
91
+ env = Environment("test")
92
+
93
+ async with env:
94
+ prompts = await env.list_prompts()
95
+
96
+ assert prompts == []
97
+
98
+ @pytest.mark.asyncio
99
+ async def test_get_prompt_not_found(self) -> None:
100
+ """get_prompt raises when prompt not found."""
101
+ from hud.environment import Environment
102
+
103
+ env = Environment("test")
104
+
105
+ async with env:
106
+ with pytest.raises(ValueError, match="Prompt not found"):
107
+ await env.get_prompt("nonexistent")
108
+
109
+
110
+ class TestEnvironmentSetupEvaluate:
111
+ """Tests for setup_tool and evaluate_tool methods."""
112
+
113
+ def test_setup_tool_with_name_and_kwargs(self) -> None:
114
+ """setup_tool accepts name and kwargs."""
115
+ from hud.environment import Environment
116
+
117
+ env = Environment("test")
118
+ env.setup_tool("navigate", url="https://example.com")
119
+
120
+ assert len(env._setup_calls) == 1
121
+ assert env._setup_calls[0] == ("navigate", {"url": "https://example.com"})
122
+
123
+ def test_setup_tool_returns_self(self) -> None:
124
+ """setup_tool returns self for chaining."""
125
+ from hud.environment import Environment
126
+
127
+ env = Environment("test")
128
+ result = env.setup_tool("navigate", url="https://example.com")
129
+
130
+ assert result is env
131
+
132
+ def test_evaluate_tool_with_name_and_kwargs(self) -> None:
133
+ """evaluate_tool accepts name and kwargs."""
134
+ from hud.environment import Environment
135
+
136
+ env = Environment("test")
137
+ env.evaluate_tool("check_text", contains="success")
138
+
139
+ assert len(env._evaluate_calls) == 1
140
+ assert env._evaluate_calls[0] == ("check_text", {"contains": "success"})
141
+
142
+ def test_evaluate_tool_returns_self(self) -> None:
143
+ """evaluate_tool returns self for chaining."""
144
+ from hud.environment import Environment
145
+
146
+ env = Environment("test")
147
+ result = env.evaluate_tool("check_text", contains="success")
148
+
149
+ assert result is env
150
+
151
+ def test_chaining_multiple_setup_calls(self) -> None:
152
+ """Multiple setup_tool calls can be chained."""
153
+ from hud.environment import Environment
154
+
155
+ env = (
156
+ Environment("test")
157
+ .setup_tool("navigate", url="https://example.com")
158
+ .setup_tool("wait", seconds=2)
159
+ )
160
+
161
+ assert len(env._setup_calls) == 2