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.
- hud/__init__.py +27 -7
- hud/agents/__init__.py +70 -5
- hud/agents/base.py +238 -500
- hud/agents/claude.py +236 -247
- hud/agents/gateway.py +42 -0
- hud/agents/gemini.py +264 -0
- hud/agents/gemini_cua.py +324 -0
- hud/agents/grounded_openai.py +98 -100
- hud/agents/misc/integration_test_agent.py +51 -20
- hud/agents/misc/response_agent.py +48 -36
- hud/agents/openai.py +282 -296
- hud/agents/{openai_chat_generic.py → openai_chat.py} +63 -33
- hud/agents/operator.py +199 -0
- hud/agents/resolver.py +70 -0
- hud/agents/tests/conftest.py +133 -0
- hud/agents/tests/test_base.py +300 -622
- hud/agents/tests/test_base_runtime.py +233 -0
- hud/agents/tests/test_claude.py +381 -214
- hud/agents/tests/test_client.py +9 -10
- hud/agents/tests/test_gemini.py +369 -0
- hud/agents/tests/test_grounded_openai_agent.py +65 -50
- hud/agents/tests/test_openai.py +377 -140
- hud/agents/tests/test_operator.py +362 -0
- hud/agents/tests/test_resolver.py +192 -0
- hud/agents/tests/test_run_eval.py +179 -0
- hud/agents/types.py +148 -0
- hud/cli/__init__.py +493 -546
- hud/cli/analyze.py +43 -5
- hud/cli/build.py +699 -113
- hud/cli/debug.py +8 -5
- hud/cli/dev.py +889 -732
- hud/cli/eval.py +793 -667
- hud/cli/flows/dev.py +167 -0
- hud/cli/flows/init.py +191 -0
- hud/cli/flows/tasks.py +153 -56
- hud/cli/flows/templates.py +151 -0
- hud/cli/flows/tests/__init__.py +1 -0
- hud/cli/flows/tests/test_dev.py +126 -0
- hud/cli/init.py +60 -58
- hud/cli/pull.py +1 -1
- hud/cli/push.py +38 -13
- hud/cli/rft.py +311 -0
- hud/cli/rft_status.py +145 -0
- hud/cli/tests/test_analyze.py +5 -5
- hud/cli/tests/test_analyze_metadata.py +3 -2
- hud/cli/tests/test_analyze_module.py +120 -0
- hud/cli/tests/test_build.py +110 -8
- hud/cli/tests/test_build_failure.py +41 -0
- hud/cli/tests/test_build_module.py +50 -0
- hud/cli/tests/test_cli_init.py +6 -1
- hud/cli/tests/test_cli_more_wrappers.py +30 -0
- hud/cli/tests/test_cli_root.py +140 -0
- hud/cli/tests/test_convert.py +361 -0
- hud/cli/tests/test_debug.py +12 -10
- hud/cli/tests/test_dev.py +197 -0
- hud/cli/tests/test_eval.py +251 -0
- hud/cli/tests/test_eval_bedrock.py +51 -0
- hud/cli/tests/test_init.py +124 -0
- hud/cli/tests/test_main_module.py +11 -5
- hud/cli/tests/test_mcp_server.py +12 -100
- hud/cli/tests/test_push.py +1 -1
- hud/cli/tests/test_push_happy.py +74 -0
- hud/cli/tests/test_push_wrapper.py +23 -0
- hud/cli/tests/test_registry.py +1 -1
- hud/cli/tests/test_utils.py +1 -1
- hud/cli/{rl → utils}/celebrate.py +14 -12
- hud/cli/utils/config.py +18 -1
- hud/cli/utils/docker.py +130 -4
- hud/cli/utils/env_check.py +9 -9
- hud/cli/utils/git.py +136 -0
- hud/cli/utils/interactive.py +39 -5
- hud/cli/utils/metadata.py +70 -1
- hud/cli/utils/runner.py +1 -1
- hud/cli/utils/server.py +2 -2
- hud/cli/utils/source_hash.py +3 -3
- hud/cli/utils/tasks.py +4 -1
- hud/cli/utils/tests/__init__.py +0 -0
- hud/cli/utils/tests/test_config.py +58 -0
- hud/cli/utils/tests/test_docker.py +93 -0
- hud/cli/utils/tests/test_docker_hints.py +71 -0
- hud/cli/utils/tests/test_env_check.py +74 -0
- hud/cli/utils/tests/test_environment.py +42 -0
- hud/cli/utils/tests/test_git.py +142 -0
- hud/cli/utils/tests/test_interactive_module.py +60 -0
- hud/cli/utils/tests/test_local_runner.py +50 -0
- hud/cli/utils/tests/test_logging_utils.py +23 -0
- hud/cli/utils/tests/test_metadata.py +49 -0
- hud/cli/utils/tests/test_package_runner.py +35 -0
- hud/cli/utils/tests/test_registry_utils.py +49 -0
- hud/cli/utils/tests/test_remote_runner.py +25 -0
- hud/cli/utils/tests/test_runner_modules.py +52 -0
- hud/cli/utils/tests/test_source_hash.py +36 -0
- hud/cli/utils/tests/test_tasks.py +80 -0
- hud/cli/utils/version_check.py +258 -0
- hud/cli/{rl → utils}/viewer.py +2 -2
- hud/clients/README.md +12 -11
- hud/clients/__init__.py +4 -3
- hud/clients/base.py +166 -26
- hud/clients/environment.py +51 -0
- hud/clients/fastmcp.py +13 -6
- hud/clients/mcp_use.py +45 -15
- hud/clients/tests/test_analyze_scenarios.py +206 -0
- hud/clients/tests/test_protocol.py +9 -3
- hud/datasets/__init__.py +23 -20
- hud/datasets/loader.py +326 -0
- hud/datasets/runner.py +198 -105
- hud/datasets/tests/__init__.py +0 -0
- hud/datasets/tests/test_loader.py +221 -0
- hud/datasets/tests/test_utils.py +315 -0
- hud/datasets/utils.py +270 -90
- hud/environment/__init__.py +52 -0
- hud/environment/connection.py +258 -0
- hud/environment/connectors/__init__.py +33 -0
- hud/environment/connectors/base.py +68 -0
- hud/environment/connectors/local.py +177 -0
- hud/environment/connectors/mcp_config.py +137 -0
- hud/environment/connectors/openai.py +101 -0
- hud/environment/connectors/remote.py +172 -0
- hud/environment/environment.py +835 -0
- hud/environment/integrations/__init__.py +45 -0
- hud/environment/integrations/adk.py +67 -0
- hud/environment/integrations/anthropic.py +196 -0
- hud/environment/integrations/gemini.py +92 -0
- hud/environment/integrations/langchain.py +82 -0
- hud/environment/integrations/llamaindex.py +68 -0
- hud/environment/integrations/openai.py +238 -0
- hud/environment/mock.py +306 -0
- hud/environment/router.py +263 -0
- hud/environment/scenarios.py +620 -0
- hud/environment/tests/__init__.py +1 -0
- hud/environment/tests/test_connection.py +317 -0
- hud/environment/tests/test_connectors.py +205 -0
- hud/environment/tests/test_environment.py +593 -0
- hud/environment/tests/test_integrations.py +257 -0
- hud/environment/tests/test_local_connectors.py +242 -0
- hud/environment/tests/test_scenarios.py +1086 -0
- hud/environment/tests/test_tools.py +208 -0
- hud/environment/types.py +23 -0
- hud/environment/utils/__init__.py +35 -0
- hud/environment/utils/formats.py +215 -0
- hud/environment/utils/schema.py +171 -0
- hud/environment/utils/tool_wrappers.py +113 -0
- hud/eval/__init__.py +67 -0
- hud/eval/context.py +727 -0
- hud/eval/display.py +299 -0
- hud/eval/instrument.py +187 -0
- hud/eval/manager.py +533 -0
- hud/eval/parallel.py +268 -0
- hud/eval/task.py +372 -0
- hud/eval/tests/__init__.py +1 -0
- hud/eval/tests/test_context.py +178 -0
- hud/eval/tests/test_eval.py +210 -0
- hud/eval/tests/test_manager.py +152 -0
- hud/eval/tests/test_parallel.py +168 -0
- hud/eval/tests/test_task.py +291 -0
- hud/eval/types.py +65 -0
- hud/eval/utils.py +194 -0
- hud/patches/__init__.py +19 -0
- hud/patches/mcp_patches.py +308 -0
- hud/patches/warnings.py +54 -0
- hud/samples/browser.py +4 -4
- hud/server/__init__.py +2 -1
- hud/server/low_level.py +2 -1
- hud/server/router.py +164 -0
- hud/server/server.py +567 -80
- hud/server/tests/test_mcp_server_integration.py +11 -11
- hud/server/tests/test_mcp_server_more.py +1 -1
- hud/server/tests/test_server_extra.py +2 -0
- hud/settings.py +45 -3
- hud/shared/exceptions.py +36 -10
- hud/shared/hints.py +26 -1
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +40 -31
- hud/shared/tests/test_hints.py +167 -0
- hud/telemetry/__init__.py +20 -19
- hud/telemetry/exporter.py +201 -0
- hud/telemetry/instrument.py +165 -253
- hud/telemetry/tests/test_eval_telemetry.py +356 -0
- hud/telemetry/tests/test_exporter.py +258 -0
- hud/telemetry/tests/test_instrument.py +401 -0
- hud/tools/__init__.py +18 -2
- hud/tools/agent.py +223 -0
- hud/tools/apply_patch.py +639 -0
- hud/tools/base.py +54 -4
- hud/tools/bash.py +2 -2
- hud/tools/computer/__init__.py +36 -3
- hud/tools/computer/anthropic.py +2 -2
- hud/tools/computer/gemini.py +385 -0
- hud/tools/computer/hud.py +23 -6
- hud/tools/computer/openai.py +20 -21
- hud/tools/computer/qwen.py +434 -0
- hud/tools/computer/settings.py +37 -0
- hud/tools/edit.py +3 -7
- hud/tools/executors/base.py +4 -2
- hud/tools/executors/pyautogui.py +1 -1
- hud/tools/grounding/grounded_tool.py +13 -18
- hud/tools/grounding/grounder.py +10 -31
- hud/tools/grounding/tests/test_grounded_tool.py +26 -44
- hud/tools/jupyter.py +330 -0
- hud/tools/playwright.py +18 -3
- hud/tools/shell.py +308 -0
- hud/tools/tests/test_agent_tool.py +355 -0
- hud/tools/tests/test_apply_patch.py +718 -0
- hud/tools/tests/test_computer.py +4 -9
- hud/tools/tests/test_computer_actions.py +24 -2
- hud/tools/tests/test_jupyter_tool.py +181 -0
- hud/tools/tests/test_shell.py +596 -0
- hud/tools/tests/test_submit.py +85 -0
- hud/tools/tests/test_types.py +193 -0
- hud/tools/types.py +21 -1
- hud/types.py +194 -56
- hud/utils/__init__.py +2 -0
- hud/utils/env.py +67 -0
- hud/utils/hud_console.py +89 -18
- hud/utils/mcp.py +15 -58
- hud/utils/strict_schema.py +162 -0
- hud/utils/tests/test_init.py +1 -2
- hud/utils/tests/test_mcp.py +1 -28
- hud/utils/tests/test_pretty_errors.py +186 -0
- hud/utils/tests/test_tool_shorthand.py +154 -0
- hud/utils/tests/test_version.py +1 -1
- hud/utils/types.py +20 -0
- hud/version.py +1 -1
- hud_python-0.5.13.dist-info/METADATA +264 -0
- hud_python-0.5.13.dist-info/RECORD +305 -0
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/WHEEL +1 -1
- hud/agents/langchain.py +0 -261
- hud/agents/lite_llm.py +0 -72
- hud/cli/rl/__init__.py +0 -180
- hud/cli/rl/config.py +0 -101
- hud/cli/rl/display.py +0 -133
- hud/cli/rl/gpu.py +0 -63
- hud/cli/rl/gpu_utils.py +0 -321
- hud/cli/rl/local_runner.py +0 -595
- hud/cli/rl/presets.py +0 -96
- hud/cli/rl/remote_runner.py +0 -463
- hud/cli/rl/rl_api.py +0 -150
- hud/cli/rl/vllm.py +0 -177
- hud/cli/rl/wait_utils.py +0 -89
- hud/datasets/parallel.py +0 -687
- hud/misc/__init__.py +0 -1
- hud/misc/claude_plays_pokemon.py +0 -292
- hud/otel/__init__.py +0 -35
- hud/otel/collector.py +0 -142
- hud/otel/config.py +0 -181
- hud/otel/context.py +0 -570
- hud/otel/exporters.py +0 -369
- hud/otel/instrumentation.py +0 -135
- hud/otel/processors.py +0 -121
- hud/otel/tests/__init__.py +0 -1
- hud/otel/tests/test_processors.py +0 -197
- hud/rl/README.md +0 -30
- hud/rl/__init__.py +0 -1
- hud/rl/actor.py +0 -176
- hud/rl/buffer.py +0 -405
- hud/rl/chat_template.jinja +0 -101
- hud/rl/config.py +0 -192
- hud/rl/distributed.py +0 -132
- hud/rl/learner.py +0 -637
- hud/rl/tests/__init__.py +0 -1
- hud/rl/tests/test_learner.py +0 -186
- hud/rl/train.py +0 -382
- hud/rl/types.py +0 -101
- hud/rl/utils/start_vllm_server.sh +0 -30
- hud/rl/utils.py +0 -524
- hud/rl/vllm_adapter.py +0 -143
- hud/telemetry/job.py +0 -352
- hud/telemetry/replay.py +0 -74
- hud/telemetry/tests/test_replay.py +0 -40
- hud/telemetry/tests/test_trace.py +0 -63
- hud/telemetry/trace.py +0 -158
- hud/utils/agent_factories.py +0 -86
- hud/utils/async_utils.py +0 -65
- hud/utils/group_eval.py +0 -223
- hud/utils/progress.py +0 -149
- hud/utils/tasks.py +0 -127
- hud/utils/tests/test_async_utils.py +0 -173
- hud/utils/tests/test_progress.py +0 -261
- hud_python-0.4.45.dist-info/METADATA +0 -552
- hud_python-0.4.45.dist-info/RECORD +0 -228
- {hud_python-0.4.45.dist-info → hud_python-0.5.13.dist-info}/entry_points.txt +0 -0
- {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"}
|