hud-python 0.4.20__py3-none-any.whl → 0.4.22__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.
Potentially problematic release.
This version of hud-python might be problematic. Click here for more details.
- hud/__init__.py +7 -0
- hud/agents/base.py +42 -10
- hud/agents/claude.py +24 -14
- hud/agents/grounded_openai.py +280 -0
- hud/agents/tests/test_client.py +11 -27
- hud/agents/tests/test_grounded_openai_agent.py +155 -0
- hud/cli/__init__.py +50 -20
- hud/cli/build.py +3 -44
- hud/cli/eval.py +25 -6
- hud/cli/init.py +4 -4
- hud/cli/push.py +3 -1
- hud/cli/tests/test_push.py +6 -6
- hud/cli/utils/interactive.py +1 -1
- hud/clients/__init__.py +3 -2
- hud/clients/base.py +20 -9
- hud/clients/mcp_use.py +44 -22
- hud/datasets/task.py +6 -2
- hud/native/__init__.py +6 -0
- hud/native/comparator.py +546 -0
- hud/native/tests/__init__.py +1 -0
- hud/native/tests/test_comparator.py +539 -0
- hud/native/tests/test_native_init.py +79 -0
- hud/otel/instrumentation.py +0 -2
- hud/server/server.py +9 -2
- hud/settings.py +6 -0
- hud/shared/exceptions.py +204 -31
- hud/shared/hints.py +177 -0
- hud/shared/requests.py +15 -3
- hud/shared/tests/test_exceptions.py +385 -144
- hud/tools/__init__.py +2 -0
- hud/tools/executors/tests/test_base_executor.py +1 -1
- hud/tools/executors/xdo.py +1 -1
- hud/tools/grounding/__init__.py +13 -0
- hud/tools/grounding/config.py +54 -0
- hud/tools/grounding/grounded_tool.py +314 -0
- hud/tools/grounding/grounder.py +301 -0
- hud/tools/grounding/tests/__init__.py +1 -0
- hud/tools/grounding/tests/test_grounded_tool.py +196 -0
- hud/tools/submit.py +66 -0
- hud/tools/tests/test_playwright_tool.py +1 -1
- hud/tools/tests/test_tools_init.py +1 -1
- hud/tools/tests/test_utils.py +2 -2
- hud/types.py +33 -5
- hud/utils/agent_factories.py +86 -0
- hud/utils/design.py +57 -0
- hud/utils/mcp.py +6 -0
- hud/utils/pretty_errors.py +68 -0
- hud/utils/tests/test_version.py +1 -1
- hud/version.py +1 -1
- {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/METADATA +2 -4
- {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/RECORD +54 -37
- {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/WHEEL +0 -0
- {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/entry_points.txt +0 -0
- {hud_python-0.4.20.dist-info → hud_python-0.4.22.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,179 +1,420 @@
|
|
|
1
|
-
"""Tests for
|
|
1
|
+
"""Tests for the HUD SDK Exception System.
|
|
2
|
+
|
|
3
|
+
This module tests the intelligent exception handling with automatic error
|
|
4
|
+
classification and helpful hints for users.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
import json
|
|
10
|
+
from unittest.mock import Mock, patch
|
|
6
11
|
|
|
7
12
|
import httpx
|
|
13
|
+
import pytest
|
|
8
14
|
|
|
9
15
|
from hud.shared.exceptions import (
|
|
10
|
-
GymMakeException,
|
|
11
16
|
HudAuthenticationError,
|
|
17
|
+
HudClientError,
|
|
18
|
+
HudConfigError,
|
|
12
19
|
HudException,
|
|
13
|
-
|
|
20
|
+
HudRateLimitError,
|
|
14
21
|
HudRequestError,
|
|
15
22
|
HudTimeoutError,
|
|
23
|
+
HudToolNotFoundError,
|
|
24
|
+
)
|
|
25
|
+
from hud.shared.hints import (
|
|
26
|
+
CLIENT_NOT_INITIALIZED,
|
|
27
|
+
HUD_API_KEY_MISSING,
|
|
28
|
+
INVALID_CONFIG,
|
|
29
|
+
RATE_LIMIT_HIT,
|
|
30
|
+
TOOL_NOT_FOUND,
|
|
16
31
|
)
|
|
17
32
|
|
|
18
33
|
|
|
19
|
-
class
|
|
20
|
-
"""Test
|
|
21
|
-
|
|
22
|
-
def
|
|
23
|
-
"""Test
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
34
|
+
class TestHudExceptionAutoConversion:
|
|
35
|
+
"""Test automatic exception conversion via 'raise HudException() from e'."""
|
|
36
|
+
|
|
37
|
+
def test_client_not_initialized_error(self):
|
|
38
|
+
"""Test that 'not initialized' errors become HudClientError."""
|
|
39
|
+
try:
|
|
40
|
+
raise ValueError("Client not initialized - call initialize() first")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
with pytest.raises(HudClientError) as exc_info:
|
|
43
|
+
raise HudException from e
|
|
44
|
+
|
|
45
|
+
assert exc_info.value.hints == [CLIENT_NOT_INITIALIZED]
|
|
46
|
+
assert str(exc_info.value) == "Client not initialized - call initialize() first"
|
|
47
|
+
|
|
48
|
+
def test_not_connected_error(self):
|
|
49
|
+
"""Test that 'not connected' errors become HudClientError."""
|
|
50
|
+
try:
|
|
51
|
+
raise RuntimeError("Session not connected to server")
|
|
52
|
+
except Exception as e:
|
|
53
|
+
with pytest.raises(HudClientError) as exc_info:
|
|
54
|
+
raise HudException from e
|
|
55
|
+
|
|
56
|
+
assert exc_info.value.hints == [CLIENT_NOT_INITIALIZED]
|
|
57
|
+
|
|
58
|
+
def test_config_invalid_json_error(self):
|
|
59
|
+
"""Test that JSON errors become HudConfigError."""
|
|
60
|
+
try:
|
|
61
|
+
json.loads("{invalid json}")
|
|
62
|
+
except json.JSONDecodeError as e:
|
|
63
|
+
with pytest.raises(HudConfigError) as exc_info:
|
|
64
|
+
raise HudException from e
|
|
65
|
+
|
|
66
|
+
assert exc_info.value.hints == [INVALID_CONFIG]
|
|
67
|
+
|
|
68
|
+
def test_config_error_keyword(self):
|
|
69
|
+
"""Test that errors with 'config' become HudConfigError."""
|
|
70
|
+
try:
|
|
71
|
+
raise ValueError("Invalid config: missing required field 'url'")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
with pytest.raises(HudConfigError) as exc_info:
|
|
74
|
+
raise HudException from e
|
|
75
|
+
|
|
76
|
+
assert exc_info.value.hints == [INVALID_CONFIG]
|
|
77
|
+
|
|
78
|
+
def test_tool_not_found_error(self):
|
|
79
|
+
"""Test that tool not found errors become HudToolNotFoundError."""
|
|
80
|
+
try:
|
|
81
|
+
raise KeyError("Tool 'missing_tool' not found in registry")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
with pytest.raises(HudToolNotFoundError) as exc_info:
|
|
84
|
+
raise HudException from e
|
|
85
|
+
|
|
86
|
+
assert exc_info.value.hints == [TOOL_NOT_FOUND]
|
|
87
|
+
|
|
88
|
+
def test_tool_not_exist_error(self):
|
|
89
|
+
"""Test that tool not exist errors become HudToolNotFoundError."""
|
|
90
|
+
try:
|
|
91
|
+
raise RuntimeError("Tool does not exist: calculator")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
with pytest.raises(HudToolNotFoundError) as exc_info:
|
|
94
|
+
raise HudException from e
|
|
95
|
+
|
|
96
|
+
assert exc_info.value.hints == [TOOL_NOT_FOUND]
|
|
97
|
+
|
|
98
|
+
def test_hud_api_key_error(self):
|
|
99
|
+
"""Test that HUD API key errors become HudAuthenticationError."""
|
|
100
|
+
try:
|
|
101
|
+
raise ValueError("API key missing for mcp.hud.so")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
with pytest.raises(HudAuthenticationError) as exc_info:
|
|
104
|
+
raise HudException from e
|
|
105
|
+
|
|
106
|
+
assert exc_info.value.hints == [HUD_API_KEY_MISSING]
|
|
107
|
+
|
|
108
|
+
def test_hud_authorization_error(self):
|
|
109
|
+
"""Test that HUD authorization errors become HudAuthenticationError."""
|
|
110
|
+
try:
|
|
111
|
+
raise PermissionError("Authorization failed for HUD API")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
with pytest.raises(HudAuthenticationError) as exc_info:
|
|
114
|
+
raise HudException from e
|
|
115
|
+
|
|
116
|
+
assert exc_info.value.hints == [HUD_API_KEY_MISSING]
|
|
117
|
+
|
|
118
|
+
def test_rate_limit_error(self):
|
|
119
|
+
"""Test that rate limit errors become HudRateLimitError."""
|
|
120
|
+
try:
|
|
121
|
+
raise RuntimeError("Rate limit exceeded")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
with pytest.raises(HudRateLimitError) as exc_info:
|
|
124
|
+
raise HudException from e
|
|
125
|
+
|
|
126
|
+
assert exc_info.value.hints == [RATE_LIMIT_HIT]
|
|
127
|
+
|
|
128
|
+
def test_too_many_requests_error(self):
|
|
129
|
+
"""Test that 'too many request' errors become HudRateLimitError."""
|
|
130
|
+
try:
|
|
131
|
+
raise httpx.HTTPStatusError("Too many requests", request=Mock(), response=Mock())
|
|
132
|
+
except Exception as e:
|
|
133
|
+
with pytest.raises(HudRateLimitError) as exc_info:
|
|
134
|
+
raise HudException from e
|
|
135
|
+
|
|
136
|
+
assert exc_info.value.hints == [RATE_LIMIT_HIT]
|
|
137
|
+
|
|
138
|
+
def test_timeout_error(self):
|
|
139
|
+
"""Test that TimeoutError becomes HudTimeoutError."""
|
|
140
|
+
try:
|
|
141
|
+
raise TimeoutError("Operation timed out")
|
|
142
|
+
except Exception as e:
|
|
143
|
+
with pytest.raises(HudTimeoutError) as exc_info:
|
|
144
|
+
raise HudException from e
|
|
145
|
+
|
|
146
|
+
assert exc_info.value.hints == [] # No default hints for timeout
|
|
147
|
+
|
|
148
|
+
def test_asyncio_timeout_error(self):
|
|
149
|
+
"""Test that asyncio.TimeoutError becomes HudTimeoutError."""
|
|
150
|
+
try:
|
|
151
|
+
raise TimeoutError("Async operation timed out")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
with pytest.raises(HudTimeoutError) as exc_info:
|
|
154
|
+
raise HudException from e
|
|
155
|
+
|
|
156
|
+
assert str(exc_info.value) == "Async operation timed out"
|
|
157
|
+
|
|
158
|
+
def test_generic_error_remains_hudexception(self):
|
|
159
|
+
"""Test that unmatched errors remain as base HudException."""
|
|
160
|
+
try:
|
|
161
|
+
raise ValueError("Some random error")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
with pytest.raises(HudException) as exc_info:
|
|
164
|
+
raise HudException from e
|
|
165
|
+
|
|
166
|
+
# Should be base HudException, not a subclass
|
|
167
|
+
assert type(exc_info.value) is HudException
|
|
168
|
+
assert exc_info.value.hints == []
|
|
169
|
+
|
|
170
|
+
def test_custom_message_override(self):
|
|
171
|
+
"""Test that custom message overrides the original."""
|
|
172
|
+
try:
|
|
173
|
+
raise ValueError("Original error")
|
|
174
|
+
except Exception as e:
|
|
175
|
+
with pytest.raises(HudException) as exc_info:
|
|
176
|
+
raise HudException("Custom error message") from e
|
|
177
|
+
|
|
178
|
+
assert str(exc_info.value) == "Custom error message"
|
|
179
|
+
|
|
180
|
+
def test_already_hud_exception_passthrough(self):
|
|
181
|
+
"""Test that existing HudExceptions are not re-wrapped."""
|
|
182
|
+
original = HudAuthenticationError("Already a HUD exception")
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
raise original
|
|
186
|
+
except Exception as e:
|
|
187
|
+
with pytest.raises(HudAuthenticationError) as exc_info:
|
|
188
|
+
raise HudException from e
|
|
189
|
+
|
|
190
|
+
# Should be the same instance
|
|
191
|
+
assert exc_info.value is original
|
|
104
192
|
|
|
105
|
-
error_str = str(error)
|
|
106
|
-
assert "Network failure" in error_str
|
|
107
|
-
assert "Connection refused" in error_str
|
|
108
193
|
|
|
194
|
+
class TestHudRequestError:
|
|
195
|
+
"""Test HudRequestError specific behavior."""
|
|
196
|
+
|
|
197
|
+
def test_401_adds_auth_hint(self):
|
|
198
|
+
"""Test that 401 status adds authentication hint."""
|
|
199
|
+
error = HudRequestError("Unauthorized", status_code=401)
|
|
200
|
+
assert HUD_API_KEY_MISSING in error.hints
|
|
201
|
+
|
|
202
|
+
def test_403_adds_auth_hint(self):
|
|
203
|
+
"""Test that 403 status adds authentication hint."""
|
|
204
|
+
error = HudRequestError("Forbidden", status_code=403)
|
|
205
|
+
assert HUD_API_KEY_MISSING in error.hints
|
|
206
|
+
|
|
207
|
+
def test_429_adds_rate_limit_hint(self):
|
|
208
|
+
"""Test that 429 status adds rate limit hint."""
|
|
209
|
+
error = HudRequestError("Too Many Requests", status_code=429)
|
|
210
|
+
assert RATE_LIMIT_HIT in error.hints
|
|
211
|
+
|
|
212
|
+
def test_other_status_no_default_hints(self):
|
|
213
|
+
"""Test that other status codes don't add default hints."""
|
|
214
|
+
error = HudRequestError("Server Error", status_code=500)
|
|
215
|
+
assert error.hints == []
|
|
216
|
+
|
|
217
|
+
def test_explicit_hints_override_defaults(self):
|
|
218
|
+
"""Test that explicit hints override status-based defaults."""
|
|
219
|
+
from hud.shared.hints import Hint
|
|
220
|
+
|
|
221
|
+
custom_hint = Hint(title="Custom Error", message="This is a custom hint")
|
|
222
|
+
error = HudRequestError("Unauthorized", status_code=401, hints=[custom_hint])
|
|
223
|
+
assert error.hints == [custom_hint]
|
|
224
|
+
assert HUD_API_KEY_MISSING not in error.hints
|
|
225
|
+
|
|
226
|
+
def test_from_httpx_error(self):
|
|
227
|
+
"""Test creating from HTTPx error."""
|
|
228
|
+
request = httpx.Request("GET", "https://api.test.com")
|
|
229
|
+
response = httpx.Response(404, json={"detail": "Not found"}, request=request)
|
|
230
|
+
httpx_error = httpx.HTTPStatusError("Not found", request=request, response=response)
|
|
231
|
+
|
|
232
|
+
error = HudRequestError.from_httpx_error(httpx_error, context="Testing")
|
|
233
|
+
|
|
234
|
+
assert error.status_code == 404
|
|
235
|
+
assert "Testing" in str(error)
|
|
236
|
+
assert "Not found" in str(error)
|
|
237
|
+
assert error.response_json == {"detail": "Not found"}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TestMCPErrorHandling:
|
|
241
|
+
"""Test handling of MCP-specific errors."""
|
|
242
|
+
|
|
243
|
+
@pytest.mark.asyncio
|
|
244
|
+
async def test_mcp_error_handling(self):
|
|
245
|
+
"""Test that McpError is handled appropriately."""
|
|
246
|
+
# Since McpError is imported dynamically, we'll mock it
|
|
247
|
+
with patch("hud.clients.mcp_use.McpError") as MockMcpError:
|
|
248
|
+
MockMcpError.side_effect = Exception
|
|
249
|
+
|
|
250
|
+
# Create a mock MCP error
|
|
251
|
+
mcp_error = Exception("MCP protocol error: Unknown method")
|
|
252
|
+
mcp_error.__class__.__name__ = "McpError"
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
raise mcp_error
|
|
256
|
+
except Exception as e:
|
|
257
|
+
# This would typically be caught in the client code
|
|
258
|
+
# and re-raised as HudException
|
|
259
|
+
with pytest.raises(HudException) as exc_info:
|
|
260
|
+
raise HudException from e
|
|
261
|
+
|
|
262
|
+
assert "MCP protocol error" in str(exc_info.value)
|
|
263
|
+
|
|
264
|
+
def test_mcp_tool_error_result(self):
|
|
265
|
+
"""Test handling of MCP tool execution errors (isError: true)."""
|
|
266
|
+
# Simulate an MCP tool result with error
|
|
267
|
+
tool_result = {
|
|
268
|
+
"content": [{"type": "text", "text": "Failed to fetch data: API rate limit exceeded"}],
|
|
269
|
+
"isError": True,
|
|
270
|
+
}
|
|
109
271
|
|
|
110
|
-
|
|
111
|
-
|
|
272
|
+
# In real usage, this would be checked in the client
|
|
273
|
+
if tool_result.get("isError"):
|
|
274
|
+
error_text = tool_result["content"][0]["text"]
|
|
112
275
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
276
|
+
try:
|
|
277
|
+
raise RuntimeError(error_text)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
with pytest.raises(HudRateLimitError) as exc_info:
|
|
280
|
+
raise HudException from e
|
|
116
281
|
|
|
117
|
-
|
|
118
|
-
assert "Request timed out" in error_str
|
|
119
|
-
assert "30.0" in error_str
|
|
282
|
+
assert exc_info.value.hints == [RATE_LIMIT_HIT]
|
|
120
283
|
|
|
121
|
-
def test_str_method(self):
|
|
122
|
-
"""Test string representation of HudTimeoutError."""
|
|
123
|
-
error = HudTimeoutError("Timeout occurred after 60.0 seconds")
|
|
124
284
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
assert "60.0" in error_str
|
|
285
|
+
class TestExceptionIntegration:
|
|
286
|
+
"""Test exception handling in integrated scenarios."""
|
|
128
287
|
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_client_initialization_flow(self):
|
|
290
|
+
"""Test exception flow during client initialization."""
|
|
291
|
+
from hud.clients.base import BaseHUDClient
|
|
129
292
|
|
|
130
|
-
|
|
131
|
-
|
|
293
|
+
# Mock a client that fails initialization
|
|
294
|
+
client = Mock(spec=BaseHUDClient)
|
|
132
295
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
296
|
+
# Simulate missing config
|
|
297
|
+
try:
|
|
298
|
+
if not hasattr(client, "_mcp_config"):
|
|
299
|
+
raise ValueError("MCP config not set")
|
|
300
|
+
except Exception as e:
|
|
301
|
+
with pytest.raises(HudConfigError) as exc_info:
|
|
302
|
+
raise HudException from e
|
|
140
303
|
|
|
304
|
+
assert exc_info.value.hints == [INVALID_CONFIG]
|
|
141
305
|
|
|
142
|
-
|
|
143
|
-
|
|
306
|
+
def test_json_parsing_flow(self):
|
|
307
|
+
"""Test exception flow during JSON parsing."""
|
|
308
|
+
invalid_json = '{"incomplete": '
|
|
144
309
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
310
|
+
try:
|
|
311
|
+
_ = json.loads(invalid_json)
|
|
312
|
+
except json.JSONDecodeError as e:
|
|
313
|
+
with pytest.raises(HudConfigError) as exc_info:
|
|
314
|
+
raise HudException from e
|
|
149
315
|
|
|
150
|
-
|
|
316
|
+
assert "Expecting value" in str(exc_info.value)
|
|
317
|
+
assert exc_info.value.hints == [INVALID_CONFIG]
|
|
151
318
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
319
|
+
@pytest.mark.asyncio
|
|
320
|
+
async def test_network_error_flow(self):
|
|
321
|
+
"""Test exception flow during network operations."""
|
|
322
|
+
# Simulate a connection error
|
|
323
|
+
try:
|
|
324
|
+
raise ConnectionError("Connection refused")
|
|
325
|
+
except Exception as e:
|
|
326
|
+
with pytest.raises(HudException) as exc_info:
|
|
327
|
+
raise HudException("Failed to connect to server") from e
|
|
158
328
|
|
|
329
|
+
# Should remain base HudException for generic connection errors
|
|
330
|
+
assert type(exc_info.value) is HudException
|
|
331
|
+
assert str(exc_info.value) == "Failed to connect to server"
|
|
159
332
|
|
|
160
|
-
class TestHudException:
|
|
161
|
-
"""Test base HudException class."""
|
|
162
333
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
response_data = {"error": "test error", "code": 42}
|
|
166
|
-
error = HudException("Base error message", response_data)
|
|
167
|
-
|
|
168
|
-
error_str = str(error)
|
|
169
|
-
assert "Base error message" in error_str
|
|
170
|
-
assert "error" in error_str
|
|
171
|
-
assert "test error" in error_str
|
|
334
|
+
class TestExceptionRendering:
|
|
335
|
+
"""Test how exceptions are rendered and displayed."""
|
|
172
336
|
|
|
173
|
-
def
|
|
174
|
-
"""Test
|
|
175
|
-
error =
|
|
337
|
+
def test_exception_string_representation(self):
|
|
338
|
+
"""Test __str__ method of exceptions."""
|
|
339
|
+
error = HudRequestError(
|
|
340
|
+
"Request failed", status_code=404, response_json={"error": "Not found"}
|
|
341
|
+
)
|
|
176
342
|
|
|
177
343
|
error_str = str(error)
|
|
178
|
-
assert
|
|
179
|
-
assert "
|
|
344
|
+
assert "Request failed" in error_str
|
|
345
|
+
assert "Status: 404" in error_str
|
|
346
|
+
assert "Response JSON: {'error': 'Not found'}" in error_str
|
|
347
|
+
|
|
348
|
+
def test_exception_with_hints(self):
|
|
349
|
+
"""Test that exceptions carry their hints properly."""
|
|
350
|
+
error = HudAuthenticationError("API key missing")
|
|
351
|
+
|
|
352
|
+
assert len(error.hints) == 1
|
|
353
|
+
assert error.hints[0] == HUD_API_KEY_MISSING
|
|
354
|
+
assert error.hints[0].title == "HUD API key required"
|
|
355
|
+
assert "Set HUD_API_KEY environment variable" in error.hints[0].tips[0]
|
|
356
|
+
|
|
357
|
+
def test_exception_type_preservation(self):
|
|
358
|
+
"""Test that exception types are preserved through conversion."""
|
|
359
|
+
test_cases = [
|
|
360
|
+
("Client not initialized", HudClientError),
|
|
361
|
+
("Invalid JSON config", HudConfigError),
|
|
362
|
+
("Tool 'test' not found", HudToolNotFoundError),
|
|
363
|
+
("API key missing for HUD", HudAuthenticationError),
|
|
364
|
+
("Rate limit exceeded", HudRateLimitError),
|
|
365
|
+
(TimeoutError("Timeout"), HudTimeoutError),
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
for error_msg, expected_type in test_cases:
|
|
369
|
+
try:
|
|
370
|
+
if isinstance(error_msg, Exception):
|
|
371
|
+
raise error_msg
|
|
372
|
+
else:
|
|
373
|
+
raise ValueError(error_msg)
|
|
374
|
+
except Exception as e:
|
|
375
|
+
with pytest.raises(expected_type):
|
|
376
|
+
raise HudException from e
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
class TestEdgeCases:
|
|
380
|
+
"""Test edge cases and error conditions."""
|
|
381
|
+
|
|
382
|
+
def test_none_exception_handling(self):
|
|
383
|
+
"""Test handling when no exception context exists."""
|
|
384
|
+
# When there's no active exception, should create normal HudException
|
|
385
|
+
error = HudException("No chained exception")
|
|
386
|
+
assert type(error) is HudException
|
|
387
|
+
assert str(error) == "No chained exception"
|
|
388
|
+
|
|
389
|
+
def test_baseexception_not_converted(self):
|
|
390
|
+
"""Test that BaseException (not Exception) is not converted."""
|
|
391
|
+
try:
|
|
392
|
+
raise KeyboardInterrupt("User interrupted")
|
|
393
|
+
except BaseException:
|
|
394
|
+
# Should not attempt to convert BaseException
|
|
395
|
+
error = HudException("Interrupted")
|
|
396
|
+
assert type(error) is HudException
|
|
397
|
+
|
|
398
|
+
def test_empty_error_message(self):
|
|
399
|
+
"""Test handling of empty error messages."""
|
|
400
|
+
try:
|
|
401
|
+
raise ValueError("")
|
|
402
|
+
except Exception as e:
|
|
403
|
+
with pytest.raises(HudException) as exc_info:
|
|
404
|
+
raise HudException from e
|
|
405
|
+
|
|
406
|
+
# Should still have some message
|
|
407
|
+
assert str(exc_info.value) != ""
|
|
408
|
+
|
|
409
|
+
def test_circular_exception_chain(self):
|
|
410
|
+
"""Test that we don't create circular exception chains."""
|
|
411
|
+
original = HudAuthenticationError("Original")
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
raise original
|
|
415
|
+
except HudException as e:
|
|
416
|
+
# Raising HudException from HudException should not re-wrap
|
|
417
|
+
with pytest.raises(HudAuthenticationError) as exc_info:
|
|
418
|
+
raise HudException from e
|
|
419
|
+
|
|
420
|
+
assert exc_info.value is original
|
hud/tools/__init__.py
CHANGED
|
@@ -9,6 +9,7 @@ from .bash import BashTool
|
|
|
9
9
|
from .edit import EditTool
|
|
10
10
|
from .playwright import PlaywrightTool
|
|
11
11
|
from .response import ResponseTool
|
|
12
|
+
from .submit import SubmitTool
|
|
12
13
|
|
|
13
14
|
if TYPE_CHECKING:
|
|
14
15
|
from .computer import AnthropicComputerTool, HudComputerTool, OpenAIComputerTool
|
|
@@ -23,6 +24,7 @@ __all__ = [
|
|
|
23
24
|
"OpenAIComputerTool",
|
|
24
25
|
"PlaywrightTool",
|
|
25
26
|
"ResponseTool",
|
|
27
|
+
"SubmitTool",
|
|
26
28
|
]
|
|
27
29
|
|
|
28
30
|
|
|
@@ -361,5 +361,5 @@ class TestLazyImports:
|
|
|
361
361
|
"""Test lazy import with invalid attribute name."""
|
|
362
362
|
import hud.tools.executors as executors_module
|
|
363
363
|
|
|
364
|
-
with pytest.raises(AttributeError, match="module '.*' has no attribute 'InvalidExecutor'"):
|
|
364
|
+
with pytest.raises(AttributeError, match=r"module '.*' has no attribute 'InvalidExecutor'"):
|
|
365
365
|
_ = executors_module.InvalidExecutor
|
hud/tools/executors/xdo.py
CHANGED
|
@@ -175,7 +175,7 @@ class XDOExecutor(BaseExecutor):
|
|
|
175
175
|
|
|
176
176
|
screenshot_cmd = f"{self._display_prefix}scrot -p {screenshot_path}"
|
|
177
177
|
|
|
178
|
-
returncode, _,
|
|
178
|
+
returncode, _, _stderr = await run(screenshot_cmd)
|
|
179
179
|
|
|
180
180
|
if returncode == 0 and screenshot_path.exists():
|
|
181
181
|
try:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Grounding module for visual element detection and coordinate resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .config import GrounderConfig
|
|
6
|
+
from .grounded_tool import GroundedComputerTool
|
|
7
|
+
from .grounder import Grounder
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"GroundedComputerTool",
|
|
11
|
+
"Grounder",
|
|
12
|
+
"GrounderConfig",
|
|
13
|
+
]
|