hud-python 0.4.52__py3-none-any.whl → 0.4.54__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.

Files changed (70) hide show
  1. hud/agents/base.py +9 -2
  2. hud/agents/openai_chat_generic.py +15 -3
  3. hud/agents/tests/test_base.py +15 -0
  4. hud/agents/tests/test_base_runtime.py +164 -0
  5. hud/cli/__init__.py +20 -12
  6. hud/cli/build.py +35 -27
  7. hud/cli/dev.py +13 -31
  8. hud/cli/eval.py +85 -84
  9. hud/cli/tests/test_analyze_module.py +120 -0
  10. hud/cli/tests/test_build.py +24 -2
  11. hud/cli/tests/test_build_failure.py +41 -0
  12. hud/cli/tests/test_build_module.py +50 -0
  13. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  14. hud/cli/tests/test_cli_root.py +134 -0
  15. hud/cli/tests/test_eval.py +6 -6
  16. hud/cli/tests/test_mcp_server.py +8 -7
  17. hud/cli/tests/test_push_happy.py +74 -0
  18. hud/cli/tests/test_push_wrapper.py +23 -0
  19. hud/cli/utils/docker.py +120 -1
  20. hud/cli/utils/runner.py +1 -1
  21. hud/cli/utils/tests/__init__.py +0 -0
  22. hud/cli/utils/tests/test_config.py +58 -0
  23. hud/cli/utils/tests/test_docker.py +93 -0
  24. hud/cli/utils/tests/test_docker_hints.py +71 -0
  25. hud/cli/utils/tests/test_env_check.py +74 -0
  26. hud/cli/utils/tests/test_environment.py +42 -0
  27. hud/cli/utils/tests/test_interactive_module.py +60 -0
  28. hud/cli/utils/tests/test_local_runner.py +50 -0
  29. hud/cli/utils/tests/test_logging_utils.py +23 -0
  30. hud/cli/utils/tests/test_metadata.py +49 -0
  31. hud/cli/utils/tests/test_package_runner.py +35 -0
  32. hud/cli/utils/tests/test_registry_utils.py +49 -0
  33. hud/cli/utils/tests/test_remote_runner.py +25 -0
  34. hud/cli/utils/tests/test_runner_modules.py +52 -0
  35. hud/cli/utils/tests/test_source_hash.py +36 -0
  36. hud/cli/utils/tests/test_tasks.py +80 -0
  37. hud/cli/utils/version_check.py +2 -2
  38. hud/datasets/tests/__init__.py +0 -0
  39. hud/datasets/tests/test_runner.py +106 -0
  40. hud/datasets/tests/test_utils.py +228 -0
  41. hud/otel/tests/__init__.py +0 -1
  42. hud/otel/tests/test_instrumentation.py +207 -0
  43. hud/server/tests/test_server_extra.py +2 -0
  44. hud/shared/exceptions.py +35 -4
  45. hud/shared/hints.py +25 -0
  46. hud/shared/requests.py +15 -3
  47. hud/shared/tests/test_exceptions.py +31 -23
  48. hud/shared/tests/test_hints.py +167 -0
  49. hud/telemetry/tests/test_async_context.py +242 -0
  50. hud/telemetry/tests/test_instrument.py +414 -0
  51. hud/telemetry/tests/test_job.py +609 -0
  52. hud/telemetry/tests/test_trace.py +183 -5
  53. hud/tools/computer/settings.py +2 -2
  54. hud/tools/tests/test_submit.py +85 -0
  55. hud/tools/tests/test_types.py +193 -0
  56. hud/types.py +17 -1
  57. hud/utils/agent_factories.py +1 -3
  58. hud/utils/mcp.py +1 -1
  59. hud/utils/tests/test_agent_factories.py +60 -0
  60. hud/utils/tests/test_mcp.py +4 -6
  61. hud/utils/tests/test_pretty_errors.py +186 -0
  62. hud/utils/tests/test_tasks.py +187 -0
  63. hud/utils/tests/test_tool_shorthand.py +154 -0
  64. hud/utils/tests/test_version.py +1 -1
  65. hud/version.py +1 -1
  66. {hud_python-0.4.52.dist-info → hud_python-0.4.54.dist-info}/METADATA +49 -49
  67. {hud_python-0.4.52.dist-info → hud_python-0.4.54.dist-info}/RECORD +70 -32
  68. {hud_python-0.4.52.dist-info → hud_python-0.4.54.dist-info}/WHEEL +0 -0
  69. {hud_python-0.4.52.dist-info → hud_python-0.4.54.dist-info}/entry_points.txt +0 -0
  70. {hud_python-0.4.52.dist-info → hud_python-0.4.54.dist-info}/licenses/LICENSE +0 -0
@@ -17,7 +17,6 @@ from hud.shared.exceptions import (
17
17
  HudClientError,
18
18
  HudConfigError,
19
19
  HudException,
20
- HudMCPError,
21
20
  HudRateLimitError,
22
21
  HudRequestError,
23
22
  HudTimeoutError,
@@ -27,6 +26,7 @@ from hud.shared.hints import (
27
26
  CLIENT_NOT_INITIALIZED,
28
27
  HUD_API_KEY_MISSING,
29
28
  INVALID_CONFIG,
29
+ PRO_PLAN_REQUIRED,
30
30
  RATE_LIMIT_HIT,
31
31
  TOOL_NOT_FOUND,
32
32
  )
@@ -157,25 +157,23 @@ class TestHudExceptionAutoConversion:
157
157
  assert str(exc_info.value) == "Async operation timed out"
158
158
 
159
159
  def test_generic_error_remains_hudexception(self):
160
- """Test that unmatched errors remain as base HudException."""
160
+ """Uncategorized errors become base HudException with original message."""
161
161
  try:
162
162
  raise ValueError("Some random error")
163
163
  except Exception as e:
164
164
  with pytest.raises(HudException) as exc_info:
165
165
  raise HudException from e
166
-
167
- # Should be base HudException, not a subclass
166
+ # Should be base HudException, not subclass
168
167
  assert type(exc_info.value) is HudException
169
- assert exc_info.value.hints == []
168
+ assert str(exc_info.value) == "Some random error"
170
169
 
171
170
  def test_custom_message_override(self):
172
- """Test that custom message overrides the original."""
171
+ """Custom message should be used for categorized errors."""
173
172
  try:
174
- raise ValueError("Original error")
173
+ raise ValueError("Client not initialized - call initialize() first")
175
174
  except Exception as e:
176
- with pytest.raises(HudException) as exc_info:
175
+ with pytest.raises(HudClientError) as exc_info:
177
176
  raise HudException("Custom error message") from e
178
-
179
177
  assert str(exc_info.value) == "Custom error message"
180
178
 
181
179
  def test_already_hud_exception_passthrough(self):
@@ -205,6 +203,22 @@ class TestHudRequestError:
205
203
  error = HudRequestError("Forbidden", status_code=403)
206
204
  assert HUD_API_KEY_MISSING in error.hints
207
205
 
206
+ def test_403_pro_plan_message_sets_pro_hint(self):
207
+ """403 with Pro wording should map to PRO_PLAN_REQUIRED, not auth."""
208
+ error = HudRequestError("Feature requires Pro plan", status_code=403)
209
+ assert PRO_PLAN_REQUIRED in error.hints
210
+ assert HUD_API_KEY_MISSING not in error.hints
211
+
212
+ def test_403_pro_plan_detail_sets_pro_hint(self):
213
+ """403 with detail indicating Pro should map to PRO_PLAN_REQUIRED."""
214
+ error = HudRequestError(
215
+ "Forbidden",
216
+ status_code=403,
217
+ response_json={"detail": "Requires Pro plan"},
218
+ )
219
+ assert PRO_PLAN_REQUIRED in error.hints
220
+ assert HUD_API_KEY_MISSING not in error.hints
221
+
208
222
  def test_429_adds_rate_limit_hint(self):
209
223
  """Test that 429 status adds rate limit hint."""
210
224
  error = HudRequestError("Too Many Requests", status_code=429)
@@ -244,23 +258,19 @@ class TestMCPErrorHandling:
244
258
  @pytest.mark.asyncio
245
259
  async def test_mcp_error_handling(self):
246
260
  """Test that McpError is handled appropriately."""
247
-
248
- # Create a mock McpError class
249
- class McpError(Exception):
250
- pass
251
-
252
- # Create a mock MCP error
253
- mcp_error = McpError("MCP protocol error: Unknown method")
261
+ # Create a dynamic class named "McpError" to trigger name-based detection
262
+ McpError = type("McpError", (Exception,), {})
254
263
 
255
264
  try:
256
- raise mcp_error
265
+ raise McpError("MCP protocol error: Unknown method")
257
266
  except Exception as e:
258
267
  # This would typically be caught in the client code
259
268
  # and re-raised as HudException
260
- with pytest.raises(HudMCPError) as exc_info:
269
+ with pytest.raises(HudException) as exc_info:
261
270
  raise HudException from e
262
271
 
263
272
  assert "MCP protocol error" in str(exc_info.value)
273
+ assert "MCP protocol error" in str(exc_info.value)
264
274
 
265
275
  def test_mcp_tool_error_result(self):
266
276
  """Test handling of MCP tool execution errors (isError: true)."""
@@ -353,6 +363,7 @@ class TestExceptionRendering:
353
363
  assert len(error.hints) == 1
354
364
  assert error.hints[0] == HUD_API_KEY_MISSING
355
365
  assert error.hints[0].title == "HUD API key required"
366
+ # Hint copy evolved; keep the assertion robust to minor copy changes
356
367
  assert "Set HUD_API_KEY" in error.hints[0].tips[0]
357
368
 
358
369
  def test_exception_type_preservation(self):
@@ -397,16 +408,13 @@ class TestEdgeCases:
397
408
  assert type(error) is HudException
398
409
 
399
410
  def test_empty_error_message(self):
400
- """Test handling of empty error messages."""
411
+ """Empty message still results in a HudException instance."""
401
412
  try:
402
413
  raise ValueError("")
403
414
  except Exception as e:
404
- with pytest.raises(HudException) as exc_info:
415
+ with pytest.raises(HudException):
405
416
  raise HudException from e
406
417
 
407
- # Should still have some message
408
- assert str(exc_info.value) != ""
409
-
410
418
  def test_circular_exception_chain(self):
411
419
  """Test that we don't create circular exception chains."""
412
420
  original = HudAuthenticationError("Original")
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from hud.shared.hints import (
6
+ CLIENT_NOT_INITIALIZED,
7
+ ENV_VAR_MISSING,
8
+ HUD_API_KEY_MISSING,
9
+ INVALID_CONFIG,
10
+ MCP_SERVER_ERROR,
11
+ RATE_LIMIT_HIT,
12
+ TOOL_NOT_FOUND,
13
+ Hint,
14
+ render_hints,
15
+ )
16
+
17
+
18
+ def test_hint_objects_basic():
19
+ assert HUD_API_KEY_MISSING.title and isinstance(HUD_API_KEY_MISSING.tips, list)
20
+ assert RATE_LIMIT_HIT.code == "RATE_LIMIT"
21
+ assert TOOL_NOT_FOUND.title.startswith("Tool")
22
+ assert CLIENT_NOT_INITIALIZED.message
23
+ assert ENV_VAR_MISSING.command_examples is not None
24
+
25
+
26
+ def test_all_hint_constants():
27
+ """Test that all predefined hint constants have required fields."""
28
+ hints = [
29
+ HUD_API_KEY_MISSING,
30
+ RATE_LIMIT_HIT,
31
+ TOOL_NOT_FOUND,
32
+ CLIENT_NOT_INITIALIZED,
33
+ INVALID_CONFIG,
34
+ ENV_VAR_MISSING,
35
+ MCP_SERVER_ERROR,
36
+ ]
37
+
38
+ for hint in hints:
39
+ assert hint.title
40
+ assert hint.message
41
+ assert hint.code
42
+
43
+
44
+ def test_hint_creation():
45
+ """Test creating a custom Hint."""
46
+ hint = Hint(
47
+ title="Test Hint",
48
+ message="This is a test",
49
+ tips=["Tip 1", "Tip 2"],
50
+ docs_url="https://example.com",
51
+ command_examples=["command 1"],
52
+ code="TEST_CODE",
53
+ context=["test", "custom"],
54
+ )
55
+
56
+ assert hint.title == "Test Hint"
57
+ assert hint.message == "This is a test"
58
+ assert hint.tips and len(hint.tips) == 2
59
+ assert hint.docs_url == "https://example.com"
60
+ assert hint.command_examples and len(hint.command_examples) == 1
61
+ assert hint.code == "TEST_CODE"
62
+ assert hint.context and "test" in hint.context
63
+
64
+
65
+ def test_hint_minimal():
66
+ """Test creating a minimal Hint with only required fields."""
67
+ hint = Hint(title="Minimal", message="Just basics")
68
+
69
+ assert hint.title == "Minimal"
70
+ assert hint.message == "Just basics"
71
+ assert hint.tips is None
72
+ assert hint.docs_url is None
73
+ assert hint.command_examples is None
74
+ assert hint.code is None
75
+ assert hint.context is None
76
+
77
+
78
+ def test_render_hints_none():
79
+ """Test that render_hints handles None gracefully."""
80
+ # Should not raise
81
+ render_hints(None)
82
+
83
+
84
+ def test_render_hints_empty_list():
85
+ """Test that render_hints handles empty list gracefully."""
86
+ # Should not raise
87
+ render_hints([])
88
+
89
+
90
+ @patch("hud.utils.hud_console.hud_console")
91
+ def test_render_hints_with_tips(mock_console):
92
+ """Test rendering hints with tips."""
93
+ render_hints([HUD_API_KEY_MISSING])
94
+
95
+ # Should call warning for title/message
96
+ mock_console.warning.assert_called()
97
+ # Should call info for tips
98
+ assert mock_console.info.call_count >= 1
99
+
100
+
101
+ @patch("hud.utils.hud_console.hud_console")
102
+ def test_render_hints_with_command_examples(mock_console):
103
+ """Test rendering hints with command examples."""
104
+ render_hints([ENV_VAR_MISSING])
105
+
106
+ # Should call command_example
107
+ mock_console.command_example.assert_called()
108
+
109
+
110
+ @patch("hud.utils.hud_console.hud_console")
111
+ def test_render_hints_with_docs_url(mock_console):
112
+ """Test rendering hints with documentation URL."""
113
+ hint = Hint(
114
+ title="Test",
115
+ message="Test message",
116
+ docs_url="https://docs.example.com",
117
+ )
118
+
119
+ render_hints([hint])
120
+
121
+ # Should call link for docs URL
122
+ mock_console.link.assert_called_with("https://docs.example.com")
123
+
124
+
125
+ @patch("hud.utils.hud_console.hud_console")
126
+ def test_render_hints_same_title_and_message(mock_console):
127
+ """Test rendering hints when title equals message."""
128
+ hint = Hint(title="Same", message="Same")
129
+
130
+ render_hints([hint])
131
+
132
+ # Should only call warning once with just the message
133
+ mock_console.warning.assert_called_once_with("Same")
134
+
135
+
136
+ @patch("hud.utils.hud_console.hud_console")
137
+ def test_render_hints_different_title_and_message(mock_console):
138
+ """Test rendering hints when title differs from message."""
139
+ hint = Hint(title="Title", message="Different message")
140
+
141
+ render_hints([hint])
142
+
143
+ # Should call warning with both title and message
144
+ mock_console.warning.assert_called_once()
145
+ call_args = mock_console.warning.call_args[0][0]
146
+ assert "Title" in call_args
147
+ assert "Different message" in call_args
148
+
149
+
150
+ def test_render_hints_with_custom_design():
151
+ """Test rendering hints with custom design object."""
152
+ custom_design = MagicMock()
153
+
154
+ hint = Hint(title="Test", message="Message")
155
+ # Should not raise when custom design is provided
156
+ render_hints([hint], design=custom_design)
157
+
158
+
159
+ @patch("hud.utils.hud_console.hud_console")
160
+ def test_render_hints_handles_exception(mock_console):
161
+ """Test that render_hints handles exceptions gracefully."""
162
+ mock_console.warning.side_effect = Exception("Test error")
163
+
164
+ hint = Hint(title="Test", message="Message")
165
+
166
+ # Should not raise, just log warning
167
+ render_hints([hint])
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from hud.telemetry.async_context import async_job, async_trace
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ async def test_async_trace_basic():
12
+ """Test basic AsyncTrace usage."""
13
+ with (
14
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
15
+ patch("hud.telemetry.async_context.track_task"),
16
+ patch("hud.telemetry.async_context._print_trace_url"),
17
+ patch("hud.telemetry.async_context._print_trace_complete_url"),
18
+ ):
19
+ mock_otel_instance = MagicMock()
20
+ mock_otel.return_value = mock_otel_instance
21
+
22
+ async with async_trace("Test Task") as trace_obj:
23
+ assert trace_obj.name == "Test Task"
24
+ assert trace_obj.id is not None
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_async_trace_with_job_id():
29
+ """Test AsyncTrace with job_id parameter."""
30
+ with (
31
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
32
+ patch("hud.telemetry.async_context.track_task"),
33
+ ):
34
+ mock_otel_instance = MagicMock()
35
+ mock_otel.return_value = mock_otel_instance
36
+
37
+ async with async_trace("Test", job_id="job-123") as trace_obj:
38
+ assert trace_obj.job_id == "job-123"
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_async_trace_with_task_id():
43
+ """Test AsyncTrace with task_id parameter."""
44
+ with (
45
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
46
+ patch("hud.telemetry.async_context.track_task"),
47
+ ):
48
+ mock_otel_instance = MagicMock()
49
+ mock_otel.return_value = mock_otel_instance
50
+
51
+ async with async_trace("Test", task_id="task-456") as trace_obj:
52
+ assert trace_obj.task_id == "task-456"
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_async_trace_prints_url_without_job():
57
+ """Test AsyncTrace prints URL when not part of a job."""
58
+ with (
59
+ patch("hud.telemetry.async_context.settings") as mock_settings,
60
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
61
+ patch("hud.telemetry.async_context.track_task"),
62
+ patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
63
+ ):
64
+ mock_settings.telemetry_enabled = True
65
+ mock_settings.api_key = "test-key"
66
+ mock_otel_instance = MagicMock()
67
+ mock_otel.return_value = mock_otel_instance
68
+
69
+ async with async_trace("Test", job_id=None):
70
+ pass
71
+
72
+ # Should print trace URL
73
+ mock_print_url.assert_called_once()
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_async_trace_no_print_url_with_job():
78
+ """Test AsyncTrace doesn't print URL when part of a job."""
79
+ with (
80
+ patch("hud.telemetry.async_context.settings") as mock_settings,
81
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
82
+ patch("hud.telemetry.async_context.track_task"),
83
+ patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
84
+ ):
85
+ mock_settings.telemetry_enabled = True
86
+ mock_settings.api_key = "test-key"
87
+ mock_otel_instance = MagicMock()
88
+ mock_otel.return_value = mock_otel_instance
89
+
90
+ async with async_trace("Test", job_id="job-123"):
91
+ pass
92
+
93
+ # Should NOT print trace URL when job_id is set
94
+ mock_print_url.assert_not_called()
95
+
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_async_trace_with_exception():
99
+ """Test AsyncTrace handles exceptions."""
100
+ with (
101
+ patch("hud.telemetry.async_context.settings") as mock_settings,
102
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
103
+ patch("hud.telemetry.async_context.track_task"),
104
+ patch("hud.telemetry.async_context._print_trace_complete_url") as mock_print,
105
+ ):
106
+ # Enable telemetry for this test
107
+ mock_settings.telemetry_enabled = True
108
+ mock_settings.api_key = "test-key"
109
+
110
+ mock_otel_instance = MagicMock()
111
+ mock_otel.return_value = mock_otel_instance
112
+
113
+ with pytest.raises(ValueError):
114
+ async with async_trace("Test"):
115
+ raise ValueError("Test error")
116
+
117
+ # Should have been called with error_occurred keyword arg
118
+ mock_print.assert_called_once()
119
+ call_kwargs = mock_print.call_args[1]
120
+ assert call_kwargs["error_occurred"] is True
121
+
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_async_job_basic():
125
+ """Test basic AsyncJob usage."""
126
+ with (
127
+ patch("hud.telemetry.async_context.track_task"),
128
+ patch("hud.telemetry.async_context._print_job_url"),
129
+ patch("hud.telemetry.async_context._print_job_complete_url"),
130
+ ):
131
+ async with async_job("Test Job") as job_obj:
132
+ assert job_obj.name == "Test Job"
133
+ assert job_obj.id is not None
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_async_job_with_metadata():
138
+ """Test AsyncJob with metadata."""
139
+ with (
140
+ patch("hud.telemetry.async_context.track_task"),
141
+ patch("hud.telemetry.async_context._print_job_url"),
142
+ patch("hud.telemetry.async_context._print_job_complete_url"),
143
+ ):
144
+ async with async_job("Test", metadata={"key": "value"}) as job_obj:
145
+ assert job_obj.metadata == {"key": "value"}
146
+
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_async_job_with_dataset_link():
150
+ """Test AsyncJob with dataset_link."""
151
+ with (
152
+ patch("hud.telemetry.async_context.track_task"),
153
+ patch("hud.telemetry.async_context._print_job_url"),
154
+ patch("hud.telemetry.async_context._print_job_complete_url"),
155
+ ):
156
+ async with async_job("Test", dataset_link="test/dataset") as job_obj:
157
+ assert job_obj.dataset_link == "test/dataset"
158
+
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_async_job_with_custom_job_id():
162
+ """Test AsyncJob with custom job_id."""
163
+ with (
164
+ patch("hud.telemetry.async_context.track_task"),
165
+ patch("hud.telemetry.async_context._print_job_url"),
166
+ patch("hud.telemetry.async_context._print_job_complete_url"),
167
+ ):
168
+ async with async_job("Test", job_id="custom-id") as job_obj:
169
+ assert job_obj.id == "custom-id"
170
+
171
+
172
+ @pytest.mark.asyncio
173
+ async def test_async_job_with_exception():
174
+ """Test AsyncJob handles exceptions."""
175
+ with (
176
+ patch("hud.telemetry.async_context.track_task"),
177
+ patch("hud.telemetry.async_context._print_job_url"),
178
+ patch("hud.telemetry.async_context._print_job_complete_url") as mock_print,
179
+ ):
180
+ with pytest.raises(ValueError):
181
+ async with async_job("Test"):
182
+ raise ValueError("Job error")
183
+
184
+ # Should print with error_occurred keyword arg
185
+ mock_print.assert_called_once()
186
+ call_kwargs = mock_print.call_args[1]
187
+ assert call_kwargs["error_occurred"] is True
188
+
189
+
190
+ @pytest.mark.asyncio
191
+ async def test_async_job_status_updates():
192
+ """Test AsyncJob sends status updates."""
193
+ with (
194
+ patch("hud.telemetry.async_context.settings") as mock_settings,
195
+ patch("hud.telemetry.async_context.track_task") as mock_track,
196
+ patch("hud.telemetry.async_context._print_job_url"),
197
+ patch("hud.telemetry.async_context._print_job_complete_url"),
198
+ ):
199
+ mock_settings.telemetry_enabled = True
200
+ mock_settings.api_key = "test-key"
201
+ mock_settings.hud_telemetry_url = "https://test.com"
202
+
203
+ async with async_job("Test"):
204
+ pass
205
+
206
+ # Should have called track_task twice (running and completed)
207
+ assert mock_track.call_count == 2
208
+
209
+
210
+ @pytest.mark.asyncio
211
+ async def test_async_job_includes_dataset_link_in_status():
212
+ """Test AsyncJob includes dataset_link in status updates."""
213
+ with (
214
+ patch("hud.telemetry.async_context.settings") as mock_settings,
215
+ patch("hud.telemetry.async_context.track_task"),
216
+ patch("hud.telemetry.async_context.make_request", new_callable=AsyncMock),
217
+ patch("hud.telemetry.async_context._print_job_url"),
218
+ patch("hud.telemetry.async_context._print_job_complete_url"),
219
+ ):
220
+ mock_settings.telemetry_enabled = True
221
+ mock_settings.api_key = "test-key"
222
+ mock_settings.hud_telemetry_url = "https://test.com"
223
+
224
+ async with async_job("Test", dataset_link="test/dataset"):
225
+ pass
226
+
227
+
228
+ @pytest.mark.asyncio
229
+ async def test_async_trace_non_root():
230
+ """Test AsyncTrace with root=False."""
231
+ with (
232
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
233
+ patch("hud.telemetry.async_context.track_task") as mock_track,
234
+ ):
235
+ mock_otel_instance = MagicMock()
236
+ mock_otel.return_value = mock_otel_instance
237
+
238
+ async with async_trace("Test", root=False):
239
+ pass
240
+
241
+ # Should not track status updates for non-root traces
242
+ mock_track.assert_not_called()