hud-python 0.3.5__py3-none-any.whl → 0.4.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.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +15 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +370 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +379 -0
  45. hud/clients/fastmcp.py +222 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -420
  87. hud/tools/computer/hud.py +376 -334
  88. hud/tools/computer/openai.py +295 -292
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.1.dist-info/METADATA +476 -0
  126. hud_python-0.4.1.dist-info/RECORD +132 -0
  127. hud_python-0.4.1.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.5.dist-info/METADATA +0 -284
  190. hud_python-0.3.5.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.5.dist-info → hud_python-0.4.1.dist-info}/WHEEL +0 -0
@@ -1,157 +1,145 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import inspect
5
- import sys
6
-
7
- import pytest
8
- from mcp.types import ImageContent, TextContent
9
-
10
- from hud.tools.bash import BashTool
11
- from hud.tools.computer.hud import HudComputerTool
12
- from hud.tools.edit import EditTool
13
- from hud.tools.helper import register_instance_tool
14
-
15
-
16
- @pytest.mark.asyncio
17
- async def test_bash_tool_echo():
18
- tool = BashTool()
19
-
20
- # Monkey-patch the private _session methods so no subprocess is spawned
21
- class _FakeSession:
22
- async def run(self, cmd: str):
23
- from hud.tools.base import ToolResult
24
-
25
- return ToolResult(output=f"mocked: {cmd}")
26
-
27
- async def start(self):
28
- return None
29
-
30
- tool._session = _FakeSession() # type: ignore[attr-defined]
31
-
32
- result = await tool(command="echo hello")
33
- assert result.output == "mocked: echo hello"
34
-
35
-
36
- @pytest.mark.asyncio
37
- async def test_bash_tool_restart_and_no_command():
38
- from hud.tools.base import ToolError, ToolResult
39
-
40
- tool = BashTool()
41
-
42
- class _FakeSession:
43
- async def run(self, cmd: str):
44
- return ToolResult(output="ran")
45
-
46
- async def start(self):
47
- return None
48
-
49
- def stop(self):
50
- return None
51
-
52
- tool._session = _FakeSession() # type: ignore[attr-defined]
53
-
54
- # Monkey-patch _BashSession.start to avoid launching a real shell
55
- async def _dummy_start(self):
56
- self._started = True
57
- from types import SimpleNamespace
58
-
59
- # minimal fake process attributes used later
60
- self._process = SimpleNamespace(returncode=None)
61
-
62
- import hud.tools.bash as bash_mod
63
-
64
- bash_mod._BashSession.start = _dummy_start # type: ignore[assignment]
65
-
66
- # restart=True returns system message
67
- res = await tool(command="ignored", restart=True)
68
- assert res.system == "tool has been restarted."
69
-
70
- # Calling without command raises ToolError
71
- with pytest.raises(ToolError):
72
- await tool()
73
-
74
-
75
- @pytest.mark.asyncio
76
- @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
77
- async def test_edit_tool_flow(tmp_path):
78
- file_path = tmp_path / "demo.txt"
79
-
80
- edit = EditTool()
81
-
82
- # create
83
- res = await edit(command="create", path=str(file_path), file_text="hello\nworld\n")
84
- assert "File created" in (res.output or "")
85
-
86
- # view
87
- res = await edit(command="view", path=str(file_path))
88
- assert "hello" in (res.output or "")
89
-
90
- # replace
91
- res = await edit(command="str_replace", path=str(file_path), old_str="world", new_str="earth")
92
- assert "has been edited" in (res.output or "")
93
-
94
- # insert
95
- res = await edit(command="insert", path=str(file_path), insert_line=1, new_str="first line\n")
96
- assert res
97
-
98
-
99
- @pytest.mark.asyncio
100
- async def test_base_executor_simulation():
101
- from hud.tools.executors.base import BaseExecutor
102
-
103
- exec = BaseExecutor()
104
- res = await exec.execute("echo test")
105
- assert "SIMULATED" in (res.output or "")
106
- shot = await exec.screenshot()
107
- assert isinstance(shot, str) and len(shot) > 0
108
-
109
-
110
- @pytest.mark.asyncio
111
- @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
112
- async def test_edit_tool_view(tmp_path):
113
- # Create a temporary file
114
- p = tmp_path / "sample.txt"
115
- p.write_text("Sample content\n")
116
-
117
- tool = EditTool()
118
- result = await tool(command="view", path=str(p))
119
- assert result.output is not None
120
- assert "Sample content" in result.output
121
-
122
-
123
- @pytest.mark.asyncio
124
- async def test_computer_tool_screenshot():
125
- comp = HudComputerTool()
126
- blocks = await comp(action="screenshot")
127
- # Check that we got content blocks back
128
- assert blocks is not None
129
- assert len(blocks) > 0
130
- # Either ImageContent or TextContent is valid
131
- assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
132
-
133
-
134
- def test_register_instance_tool_signature():
135
- """Helper should expose same user-facing parameters (no *args/**kwargs)."""
136
-
137
- class Dummy:
138
- async def __call__(self, *, x: int, y: str) -> str:
139
- return f"{x}-{y}"
140
-
141
- from mcp.server.fastmcp import FastMCP
142
-
143
- mcp = FastMCP("test")
144
- fn = register_instance_tool(mcp, "dummy", Dummy())
145
- sig = inspect.signature(fn)
146
- params = list(sig.parameters.values())
147
-
148
- assert [p.name for p in params] == ["x", "y"], "*args/**kwargs should be stripped"
149
-
150
-
151
- def test_build_server_subset():
152
- """Ensure build_server registers only requested tools."""
153
- from hud.tools.helper.mcp_server import build_server
154
-
155
- mcp = build_server(["bash"])
156
- names = [t.name for t in asyncio.run(mcp.list_tools())]
157
- assert names == ["bash"]
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ import pytest
6
+ from mcp.types import ImageContent, TextContent
7
+
8
+ from hud.tools.bash import BashTool
9
+ from hud.tools.computer.hud import HudComputerTool
10
+ from hud.tools.edit import EditTool
11
+
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_bash_tool_echo():
15
+ tool = BashTool()
16
+
17
+ # Monkey-patch the private _session methods so no subprocess is spawned
18
+ from hud.tools.types import ContentResult
19
+
20
+ class _FakeSession:
21
+ async def run(self, cmd: str):
22
+ return ContentResult(output=f"mocked: {cmd}")
23
+
24
+ async def start(self):
25
+ return None
26
+
27
+ tool.session = _FakeSession() # type: ignore[assignment]
28
+
29
+ result = await tool(command="echo hello")
30
+ assert len(result) > 0
31
+ assert isinstance(result[0], TextContent)
32
+ assert result[0].text == "mocked: echo hello"
33
+
34
+
35
+ @pytest.mark.asyncio
36
+ async def test_bash_tool_restart_and_no_command():
37
+ from hud.tools.types import ToolError
38
+
39
+ tool = BashTool()
40
+
41
+ from hud.tools.types import ContentResult
42
+
43
+ class _FakeSession:
44
+ async def run(self, cmd: str):
45
+ return ContentResult(output="ran")
46
+
47
+ async def start(self):
48
+ return None
49
+
50
+ def stop(self):
51
+ return None
52
+
53
+ tool.session = _FakeSession() # type: ignore[assignment]
54
+
55
+ # Monkey-patch _BashSession.start to avoid launching a real shell
56
+ async def _dummy_start(self):
57
+ self._started = True
58
+ from types import SimpleNamespace
59
+
60
+ # minimal fake process attributes used later
61
+ self._process = SimpleNamespace(returncode=None)
62
+
63
+ import hud.tools.bash as bash_mod
64
+
65
+ bash_mod._BashSession.start = _dummy_start # type: ignore[assignment]
66
+
67
+ # restart=True returns system message
68
+ res = await tool(command="ignored", restart=True)
69
+ # Check that we get content blocks with the restart message
70
+ assert len(res) > 0
71
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
72
+ assert any("restarted" in b.text for b in text_blocks)
73
+
74
+ # Calling without command raises ToolError
75
+ with pytest.raises(ToolError):
76
+ await tool()
77
+
78
+
79
+ @pytest.mark.asyncio
80
+ @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
81
+ async def test_edit_tool_flow(tmp_path):
82
+ file_path = tmp_path / "demo.txt"
83
+
84
+ edit = EditTool()
85
+
86
+ # create
87
+ res = await edit(command="create", path=str(file_path), file_text="hello\nworld\n")
88
+ # Check for success message in content blocks
89
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
90
+ assert any("created" in b.text for b in text_blocks)
91
+
92
+ # view
93
+ res = await edit(command="view", path=str(file_path))
94
+ # Check content blocks for file content
95
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
96
+ combined_text = "".join(b.text for b in text_blocks)
97
+ assert "hello" in combined_text
98
+
99
+ # replace
100
+ res = await edit(command="str_replace", path=str(file_path), old_str="world", new_str="earth")
101
+ # Check for success message in content blocks
102
+ text_blocks = [b for b in res if isinstance(b, TextContent)]
103
+ combined_text = "".join(b.text for b in text_blocks)
104
+ assert "has been edited" in combined_text
105
+
106
+ # insert
107
+ res = await edit(command="insert", path=str(file_path), insert_line=1, new_str="first line\n")
108
+ assert res
109
+
110
+
111
+ @pytest.mark.asyncio
112
+ async def test_base_executor_simulation():
113
+ from hud.tools.executors.base import BaseExecutor
114
+
115
+ exec = BaseExecutor()
116
+ res = await exec.execute("echo test")
117
+ assert "SIMULATED" in (res.output or "")
118
+ shot = await exec.screenshot()
119
+ assert isinstance(shot, str) and len(shot) > 0
120
+
121
+
122
+ @pytest.mark.asyncio
123
+ @pytest.mark.skipif(sys.platform == "win32", reason="EditTool uses Unix commands")
124
+ async def test_edit_tool_view(tmp_path):
125
+ # Create a temporary file
126
+ p = tmp_path / "sample.txt"
127
+ p.write_text("Sample content\n")
128
+
129
+ tool = EditTool()
130
+ result = await tool(command="view", path=str(p))
131
+ # Check content blocks for file content
132
+ text_blocks = [b for b in result if isinstance(b, TextContent)]
133
+ combined_text = "".join(b.text for b in text_blocks)
134
+ assert "Sample content" in combined_text
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_computer_tool_screenshot():
139
+ comp = HudComputerTool()
140
+ blocks = await comp(action="screenshot")
141
+ # Check that we got content blocks back
142
+ assert blocks is not None
143
+ assert len(blocks) > 0
144
+ # Either ImageContent or TextContent is valid
145
+ assert all(isinstance(b, (ImageContent | TextContent)) for b in blocks)
@@ -1,156 +1,156 @@
1
- """Tests for tools utils."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- from unittest.mock import AsyncMock, patch
7
-
8
- import pytest
9
-
10
- from hud.tools.utils import maybe_truncate, run
11
-
12
-
13
- class TestRun:
14
- """Tests for the run function."""
15
-
16
- @pytest.mark.asyncio
17
- async def test_run_string_command_success(self):
18
- """Test running a string command successfully."""
19
- mock_proc = AsyncMock()
20
- mock_proc.returncode = 0
21
- mock_proc.communicate = AsyncMock(return_value=(b"output", b""))
22
-
23
- with patch("asyncio.create_subprocess_shell", return_value=mock_proc) as mock_shell:
24
- return_code, stdout, stderr = await run("echo test")
25
-
26
- assert return_code == 0
27
- assert stdout == "output"
28
- assert stderr == ""
29
- mock_shell.assert_called_once()
30
-
31
- @pytest.mark.asyncio
32
- async def test_run_list_command_success(self):
33
- """Test running a list command successfully."""
34
- mock_proc = AsyncMock()
35
- mock_proc.returncode = 0
36
- mock_proc.communicate = AsyncMock(return_value=(b"hello world", b""))
37
-
38
- with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
39
- return_code, stdout, stderr = await run(["echo", "hello", "world"])
40
-
41
- assert return_code == 0
42
- assert stdout == "hello world"
43
- assert stderr == ""
44
- mock_exec.assert_called_once_with(
45
- "echo",
46
- "hello",
47
- "world",
48
- stdin=None,
49
- stdout=asyncio.subprocess.PIPE,
50
- stderr=asyncio.subprocess.PIPE,
51
- )
52
-
53
- @pytest.mark.asyncio
54
- async def test_run_with_input(self):
55
- """Test running a command with input."""
56
- mock_proc = AsyncMock()
57
- mock_proc.returncode = 0
58
- mock_proc.communicate = AsyncMock(return_value=(b"processed", b""))
59
-
60
- with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
61
- return_code, stdout, stderr = await run("cat", input="test input")
62
-
63
- assert return_code == 0
64
- assert stdout == "processed"
65
- mock_proc.communicate.assert_called_once_with(input=b"test input")
66
-
67
- @pytest.mark.asyncio
68
- async def test_run_with_error(self):
69
- """Test running a command that returns an error."""
70
- mock_proc = AsyncMock()
71
- mock_proc.returncode = 1
72
- mock_proc.communicate = AsyncMock(return_value=(b"", b"error message"))
73
-
74
- with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
75
- return_code, stdout, stderr = await run("false")
76
-
77
- assert return_code == 1
78
- assert stdout == ""
79
- assert stderr == "error message"
80
-
81
- @pytest.mark.asyncio
82
- async def test_run_with_timeout(self):
83
- """Test running a command with custom timeout."""
84
- mock_proc = AsyncMock()
85
- mock_proc.returncode = 0
86
- mock_proc.communicate = AsyncMock(return_value=(b"done", b""))
87
-
88
- with (
89
- patch("asyncio.create_subprocess_shell", return_value=mock_proc),
90
- patch("asyncio.wait_for") as mock_wait_for,
91
- ):
92
- mock_wait_for.return_value = (b"done", b"")
93
-
94
- return_code, stdout, stderr = await run("sleep 1", timeout=5.0)
95
-
96
- # Check that wait_for was called with the correct timeout
97
- mock_wait_for.assert_called_once()
98
- assert mock_wait_for.call_args[1]["timeout"] == 5.0
99
-
100
- @pytest.mark.asyncio
101
- async def test_run_timeout_exception(self):
102
- """Test running a command that times out."""
103
- mock_proc = AsyncMock()
104
-
105
- with (
106
- patch("asyncio.create_subprocess_shell", return_value=mock_proc),
107
- patch("asyncio.wait_for", side_effect=TimeoutError()),
108
- pytest.raises(asyncio.TimeoutError),
109
- ):
110
- await run("sleep infinity", timeout=0.1)
111
-
112
-
113
- class TestMaybeTruncate:
114
- """Tests for the maybe_truncate function."""
115
-
116
- def test_maybe_truncate_short_text(self):
117
- """Test that short text is not truncated."""
118
- text = "This is a short text"
119
- result = maybe_truncate(text)
120
- assert result == text
121
-
122
- def test_maybe_truncate_long_text_default(self):
123
- """Test that long text is truncated with default limit."""
124
- text = "x" * 30000 # Much longer than default limit
125
- result = maybe_truncate(text)
126
-
127
- assert len(result) < len(text)
128
- assert result.endswith("... (truncated)")
129
- assert len(result) == 20480 + len("... (truncated)")
130
-
131
- def test_maybe_truncate_custom_limit(self):
132
- """Test truncation with custom limit."""
133
- text = "abcdefghijklmnopqrstuvwxyz"
134
- result = maybe_truncate(text, max_length=10)
135
-
136
- assert result == "abcdefghij... (truncated)"
137
-
138
- def test_maybe_truncate_exact_limit(self):
139
- """Test text exactly at limit is not truncated."""
140
- text = "x" * 100
141
- result = maybe_truncate(text, max_length=100)
142
-
143
- assert result == text
144
-
145
- def test_maybe_truncate_empty_string(self):
146
- """Test empty string handling."""
147
- result = maybe_truncate("")
148
- assert result == ""
149
-
150
- def test_maybe_truncate_unicode(self):
151
- """Test truncation with unicode characters."""
152
- text = "🎉" * 5000
153
- result = maybe_truncate(text, max_length=10)
154
-
155
- assert len(result) > 10 # Because of "... (truncated)" suffix
156
- assert result.endswith("... (truncated)")
1
+ """Tests for tools utils."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from unittest.mock import AsyncMock, patch
7
+
8
+ import pytest
9
+
10
+ from hud.tools.utils import maybe_truncate, run
11
+
12
+
13
+ class TestRun:
14
+ """Tests for the run function."""
15
+
16
+ @pytest.mark.asyncio
17
+ async def test_run_string_command_success(self):
18
+ """Test running a string command successfully."""
19
+ mock_proc = AsyncMock()
20
+ mock_proc.returncode = 0
21
+ mock_proc.communicate = AsyncMock(return_value=(b"output", b""))
22
+
23
+ with patch("asyncio.create_subprocess_shell", return_value=mock_proc) as mock_shell:
24
+ return_code, stdout, stderr = await run("echo test")
25
+
26
+ assert return_code == 0
27
+ assert stdout == "output"
28
+ assert stderr == ""
29
+ mock_shell.assert_called_once()
30
+
31
+ @pytest.mark.asyncio
32
+ async def test_run_list_command_success(self):
33
+ """Test running a list command successfully."""
34
+ mock_proc = AsyncMock()
35
+ mock_proc.returncode = 0
36
+ mock_proc.communicate = AsyncMock(return_value=(b"hello world", b""))
37
+
38
+ with patch("asyncio.create_subprocess_exec", return_value=mock_proc) as mock_exec:
39
+ return_code, stdout, stderr = await run(["echo", "hello", "world"])
40
+
41
+ assert return_code == 0
42
+ assert stdout == "hello world"
43
+ assert stderr == ""
44
+ mock_exec.assert_called_once_with(
45
+ "echo",
46
+ "hello",
47
+ "world",
48
+ stdin=None,
49
+ stdout=asyncio.subprocess.PIPE,
50
+ stderr=asyncio.subprocess.PIPE,
51
+ )
52
+
53
+ @pytest.mark.asyncio
54
+ async def test_run_with_input(self):
55
+ """Test running a command with input."""
56
+ mock_proc = AsyncMock()
57
+ mock_proc.returncode = 0
58
+ mock_proc.communicate = AsyncMock(return_value=(b"processed", b""))
59
+
60
+ with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
61
+ return_code, stdout, stderr = await run("cat", input="test input")
62
+
63
+ assert return_code == 0
64
+ assert stdout == "processed"
65
+ mock_proc.communicate.assert_called_once_with(input=b"test input")
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_run_with_error(self):
69
+ """Test running a command that returns an error."""
70
+ mock_proc = AsyncMock()
71
+ mock_proc.returncode = 1
72
+ mock_proc.communicate = AsyncMock(return_value=(b"", b"error message"))
73
+
74
+ with patch("asyncio.create_subprocess_shell", return_value=mock_proc):
75
+ return_code, stdout, stderr = await run("false")
76
+
77
+ assert return_code == 1
78
+ assert stdout == ""
79
+ assert stderr == "error message"
80
+
81
+ @pytest.mark.asyncio
82
+ async def test_run_with_timeout(self):
83
+ """Test running a command with custom timeout."""
84
+ mock_proc = AsyncMock()
85
+ mock_proc.returncode = 0
86
+ mock_proc.communicate = AsyncMock(return_value=(b"done", b""))
87
+
88
+ with (
89
+ patch("asyncio.create_subprocess_shell", return_value=mock_proc),
90
+ patch("asyncio.wait_for") as mock_wait_for,
91
+ ):
92
+ mock_wait_for.return_value = (b"done", b"")
93
+
94
+ return_code, stdout, stderr = await run("sleep 1", timeout=5.0)
95
+
96
+ # Check that wait_for was called with the correct timeout
97
+ mock_wait_for.assert_called_once()
98
+ assert mock_wait_for.call_args[1]["timeout"] == 5.0
99
+
100
+ @pytest.mark.asyncio
101
+ async def test_run_timeout_exception(self):
102
+ """Test running a command that times out."""
103
+ mock_proc = AsyncMock()
104
+
105
+ with (
106
+ patch("asyncio.create_subprocess_shell", return_value=mock_proc),
107
+ patch("asyncio.wait_for", side_effect=TimeoutError()),
108
+ pytest.raises(asyncio.TimeoutError),
109
+ ):
110
+ await run("sleep infinity", timeout=0.1)
111
+
112
+
113
+ class TestMaybeTruncate:
114
+ """Tests for the maybe_truncate function."""
115
+
116
+ def test_maybe_truncate_short_text(self):
117
+ """Test that short text is not truncated."""
118
+ text = "This is a short text"
119
+ result = maybe_truncate(text)
120
+ assert result == text
121
+
122
+ def test_maybe_truncate_long_text_default(self):
123
+ """Test that long text is truncated with default limit."""
124
+ text = "x" * 30000 # Much longer than default limit
125
+ result = maybe_truncate(text)
126
+
127
+ assert len(result) < len(text)
128
+ assert result.endswith("... (truncated)")
129
+ assert len(result) == 20480 + len("... (truncated)")
130
+
131
+ def test_maybe_truncate_custom_limit(self):
132
+ """Test truncation with custom limit."""
133
+ text = "abcdefghijklmnopqrstuvwxyz"
134
+ result = maybe_truncate(text, max_length=10)
135
+
136
+ assert result == "abcdefghij... (truncated)"
137
+
138
+ def test_maybe_truncate_exact_limit(self):
139
+ """Test text exactly at limit is not truncated."""
140
+ text = "x" * 100
141
+ result = maybe_truncate(text, max_length=100)
142
+
143
+ assert result == text
144
+
145
+ def test_maybe_truncate_empty_string(self):
146
+ """Test empty string handling."""
147
+ result = maybe_truncate("")
148
+ assert result == ""
149
+
150
+ def test_maybe_truncate_unicode(self):
151
+ """Test truncation with unicode characters."""
152
+ text = "🎉" * 5000
153
+ result = maybe_truncate(text, max_length=10)
154
+
155
+ assert len(result) > 10 # Because of "... (truncated)" suffix
156
+ assert result.endswith("... (truncated)")