hud-python 0.4.1__py3-none-any.whl → 0.4.3__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 (130) hide show
  1. hud/__init__.py +22 -22
  2. hud/agents/__init__.py +13 -15
  3. hud/agents/base.py +599 -599
  4. hud/agents/claude.py +373 -373
  5. hud/agents/langchain.py +261 -250
  6. hud/agents/misc/__init__.py +7 -7
  7. hud/agents/misc/response_agent.py +82 -80
  8. hud/agents/openai.py +352 -352
  9. hud/agents/openai_chat_generic.py +154 -154
  10. hud/agents/tests/__init__.py +1 -1
  11. hud/agents/tests/test_base.py +742 -742
  12. hud/agents/tests/test_claude.py +324 -324
  13. hud/agents/tests/test_client.py +363 -363
  14. hud/agents/tests/test_openai.py +237 -237
  15. hud/cli/__init__.py +617 -617
  16. hud/cli/__main__.py +8 -8
  17. hud/cli/analyze.py +371 -371
  18. hud/cli/analyze_metadata.py +230 -230
  19. hud/cli/build.py +498 -427
  20. hud/cli/clone.py +185 -185
  21. hud/cli/cursor.py +92 -92
  22. hud/cli/debug.py +392 -392
  23. hud/cli/docker_utils.py +83 -83
  24. hud/cli/init.py +280 -281
  25. hud/cli/interactive.py +353 -353
  26. hud/cli/mcp_server.py +764 -756
  27. hud/cli/pull.py +330 -336
  28. hud/cli/push.py +404 -370
  29. hud/cli/remote_runner.py +311 -311
  30. hud/cli/runner.py +160 -160
  31. hud/cli/tests/__init__.py +3 -3
  32. hud/cli/tests/test_analyze.py +284 -284
  33. hud/cli/tests/test_cli_init.py +265 -265
  34. hud/cli/tests/test_cli_main.py +27 -27
  35. hud/cli/tests/test_clone.py +142 -142
  36. hud/cli/tests/test_cursor.py +253 -253
  37. hud/cli/tests/test_debug.py +453 -453
  38. hud/cli/tests/test_mcp_server.py +139 -139
  39. hud/cli/tests/test_utils.py +388 -388
  40. hud/cli/utils.py +263 -263
  41. hud/clients/README.md +143 -143
  42. hud/clients/__init__.py +16 -16
  43. hud/clients/base.py +378 -379
  44. hud/clients/fastmcp.py +222 -222
  45. hud/clients/mcp_use.py +298 -278
  46. hud/clients/tests/__init__.py +1 -1
  47. hud/clients/tests/test_client_integration.py +111 -111
  48. hud/clients/tests/test_fastmcp.py +342 -342
  49. hud/clients/tests/test_protocol.py +188 -188
  50. hud/clients/utils/__init__.py +1 -1
  51. hud/clients/utils/retry_transport.py +160 -160
  52. hud/datasets.py +327 -322
  53. hud/misc/__init__.py +1 -1
  54. hud/misc/claude_plays_pokemon.py +292 -292
  55. hud/otel/__init__.py +35 -35
  56. hud/otel/collector.py +142 -142
  57. hud/otel/config.py +164 -164
  58. hud/otel/context.py +536 -536
  59. hud/otel/exporters.py +366 -366
  60. hud/otel/instrumentation.py +97 -97
  61. hud/otel/processors.py +118 -118
  62. hud/otel/tests/__init__.py +1 -1
  63. hud/otel/tests/test_processors.py +197 -197
  64. hud/server/__init__.py +5 -5
  65. hud/server/context.py +114 -114
  66. hud/server/helper/__init__.py +5 -5
  67. hud/server/low_level.py +132 -132
  68. hud/server/server.py +170 -166
  69. hud/server/tests/__init__.py +3 -3
  70. hud/settings.py +73 -73
  71. hud/shared/__init__.py +5 -5
  72. hud/shared/exceptions.py +180 -180
  73. hud/shared/requests.py +264 -264
  74. hud/shared/tests/test_exceptions.py +157 -157
  75. hud/shared/tests/test_requests.py +275 -275
  76. hud/telemetry/__init__.py +25 -25
  77. hud/telemetry/instrument.py +379 -379
  78. hud/telemetry/job.py +309 -309
  79. hud/telemetry/replay.py +74 -74
  80. hud/telemetry/trace.py +83 -83
  81. hud/tools/__init__.py +33 -33
  82. hud/tools/base.py +365 -365
  83. hud/tools/bash.py +161 -161
  84. hud/tools/computer/__init__.py +15 -15
  85. hud/tools/computer/anthropic.py +437 -437
  86. hud/tools/computer/hud.py +376 -376
  87. hud/tools/computer/openai.py +295 -295
  88. hud/tools/computer/settings.py +82 -82
  89. hud/tools/edit.py +314 -314
  90. hud/tools/executors/__init__.py +30 -30
  91. hud/tools/executors/base.py +539 -539
  92. hud/tools/executors/pyautogui.py +621 -621
  93. hud/tools/executors/tests/__init__.py +1 -1
  94. hud/tools/executors/tests/test_base_executor.py +338 -338
  95. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  96. hud/tools/executors/xdo.py +511 -511
  97. hud/tools/playwright.py +412 -412
  98. hud/tools/tests/__init__.py +3 -3
  99. hud/tools/tests/test_base.py +282 -282
  100. hud/tools/tests/test_bash.py +158 -158
  101. hud/tools/tests/test_bash_extended.py +197 -197
  102. hud/tools/tests/test_computer.py +425 -425
  103. hud/tools/tests/test_computer_actions.py +34 -34
  104. hud/tools/tests/test_edit.py +259 -259
  105. hud/tools/tests/test_init.py +27 -27
  106. hud/tools/tests/test_playwright_tool.py +183 -183
  107. hud/tools/tests/test_tools.py +145 -145
  108. hud/tools/tests/test_utils.py +156 -156
  109. hud/tools/types.py +72 -72
  110. hud/tools/utils.py +50 -50
  111. hud/types.py +136 -136
  112. hud/utils/__init__.py +10 -10
  113. hud/utils/async_utils.py +65 -65
  114. hud/utils/design.py +236 -168
  115. hud/utils/mcp.py +55 -55
  116. hud/utils/progress.py +149 -149
  117. hud/utils/telemetry.py +66 -66
  118. hud/utils/tests/test_async_utils.py +173 -173
  119. hud/utils/tests/test_init.py +17 -17
  120. hud/utils/tests/test_progress.py +261 -261
  121. hud/utils/tests/test_telemetry.py +82 -82
  122. hud/utils/tests/test_version.py +8 -8
  123. hud/version.py +7 -7
  124. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/METADATA +10 -8
  125. hud_python-0.4.3.dist-info/RECORD +131 -0
  126. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/licenses/LICENSE +21 -21
  127. hud/agents/art.py +0 -101
  128. hud_python-0.4.1.dist-info/RECORD +0 -132
  129. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/WHEEL +0 -0
  130. {hud_python-0.4.1.dist-info → hud_python-0.4.3.dist-info}/entry_points.txt +0 -0
@@ -1,253 +1,253 @@
1
- """Tests for hud.cli.cursor module."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- from pathlib import Path
8
- from unittest.mock import mock_open, patch
9
-
10
- import pytest
11
-
12
- from hud.cli.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
13
-
14
-
15
- class TestParseCursorConfig:
16
- """Test Cursor config parsing."""
17
-
18
- def test_parse_cursor_config_success(self) -> None:
19
- """Test successful parsing of Cursor config."""
20
- config_data = {
21
- "mcpServers": {
22
- "test-server": {
23
- "command": "python",
24
- "args": ["server.py", "--port", "8080"],
25
- "env": {"KEY": "value"},
26
- }
27
- }
28
- }
29
-
30
- with (
31
- patch("pathlib.Path.exists", return_value=True),
32
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
33
- ):
34
- command, error = parse_cursor_config("test-server")
35
- assert error is None
36
- assert command == ["python", "server.py", "--port", "8080"]
37
-
38
- def test_parse_cursor_config_not_found(self) -> None:
39
- """Test parsing when config file doesn't exist."""
40
- with patch("pathlib.Path.exists", return_value=False):
41
- command, error = parse_cursor_config("test-server")
42
- assert command is None
43
- assert error is not None
44
- assert "Cursor config not found" in error
45
-
46
- def test_parse_cursor_config_server_not_found(self) -> None:
47
- """Test parsing when server doesn't exist in config."""
48
- config_data = {"mcpServers": {"other-server": {"command": "node", "args": ["server.js"]}}}
49
-
50
- with (
51
- patch("pathlib.Path.exists", return_value=True),
52
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
53
- ):
54
- command, error = parse_cursor_config("test-server")
55
- assert command is None
56
- assert error is not None
57
- assert "Server 'test-server' not found" in error
58
- assert "Available: other-server" in error
59
-
60
- def test_parse_cursor_config_reloaderoo(self) -> None:
61
- """Test parsing config with reloaderoo wrapper."""
62
- config_data = {
63
- "mcpServers": {
64
- "test-server": {
65
- "command": "npx",
66
- "args": ["reloaderoo", "--watch", "src", "--", "python", "server.py"],
67
- }
68
- }
69
- }
70
-
71
- with (
72
- patch("pathlib.Path.exists", return_value=True),
73
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
74
- ):
75
- command, error = parse_cursor_config("test-server")
76
- assert error is None
77
- # Should extract command after --
78
- assert command == ["python", "server.py"]
79
-
80
- def test_parse_cursor_config_reloaderoo_no_dash(self) -> None:
81
- """Test parsing reloaderoo without -- separator."""
82
- config_data = {
83
- "mcpServers": {
84
- "test-server": {"command": "npx", "args": ["reloaderoo", "python", "server.py"]}
85
- }
86
- }
87
-
88
- with (
89
- patch("pathlib.Path.exists", return_value=True),
90
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
91
- ):
92
- command, error = parse_cursor_config("test-server")
93
- assert error is None
94
- # Should return full command since no -- found
95
- assert command == ["npx", "reloaderoo", "python", "server.py"]
96
-
97
- def test_parse_cursor_config_windows_path(self) -> None:
98
- """Test parsing with Windows user profile path."""
99
- config_data = {"mcpServers": {"test": {"command": "cmd"}}}
100
-
101
- # First path doesn't exist, try Windows path
102
- with (
103
- patch("pathlib.Path.exists", side_effect=[False, True]),
104
- patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
105
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
106
- ):
107
- command, error = parse_cursor_config("test")
108
- assert error is None
109
- assert command == ["cmd"]
110
-
111
- def test_parse_cursor_config_json_error(self) -> None:
112
- """Test parsing with invalid JSON."""
113
- with (
114
- patch("pathlib.Path.exists", return_value=True),
115
- patch("builtins.open", mock_open(read_data="invalid json")),
116
- ):
117
- command, error = parse_cursor_config("test-server")
118
- assert command is None
119
- assert error is not None
120
- assert "Error reading config" in error
121
-
122
- def test_parse_cursor_config_no_command(self) -> None:
123
- """Test parsing server with no command."""
124
- config_data = {"mcpServers": {"test-server": {"args": ["--port", "8080"]}}}
125
-
126
- with (
127
- patch("pathlib.Path.exists", return_value=True),
128
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
129
- ):
130
- command, error = parse_cursor_config("test-server")
131
- assert error is None
132
- assert command == ["", "--port", "8080"] # Empty command
133
-
134
-
135
- class TestListCursorServers:
136
- """Test listing Cursor servers."""
137
-
138
- def test_list_cursor_servers_success(self) -> None:
139
- """Test successful listing of servers."""
140
- config_data = {
141
- "mcpServers": {
142
- "server1": {"command": "python"},
143
- "server2": {"command": "node"},
144
- "server3": {"command": "ruby"},
145
- }
146
- }
147
-
148
- with (
149
- patch("pathlib.Path.exists", return_value=True),
150
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
151
- ):
152
- servers, error = list_cursor_servers()
153
- assert error is None
154
- assert servers == ["server1", "server2", "server3"]
155
-
156
- def test_list_cursor_servers_empty(self) -> None:
157
- """Test listing when no servers configured."""
158
- config_data = {"mcpServers": {}}
159
-
160
- with (
161
- patch("pathlib.Path.exists", return_value=True),
162
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
163
- ):
164
- servers, error = list_cursor_servers()
165
- assert error is None
166
- assert servers == []
167
-
168
- def test_list_cursor_servers_no_mcp_section(self) -> None:
169
- """Test listing when mcpServers section missing."""
170
- config_data = {"otherConfig": {}}
171
-
172
- with (
173
- patch("pathlib.Path.exists", return_value=True),
174
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
175
- ):
176
- servers, error = list_cursor_servers()
177
- assert error is None
178
- assert servers == []
179
-
180
- def test_list_cursor_servers_file_not_found(self) -> None:
181
- """Test listing when config file doesn't exist."""
182
- with patch("pathlib.Path.exists", return_value=False):
183
- servers, error = list_cursor_servers()
184
- assert servers is None
185
- assert error is not None
186
- assert "Cursor config not found" in error
187
-
188
- def test_list_cursor_servers_windows_path(self) -> None:
189
- """Test listing with Windows path fallback."""
190
- config_data = {"mcpServers": {"winserver": {"command": "cmd"}}}
191
-
192
- # First path doesn't exist, second (Windows) does
193
- with (
194
- patch("pathlib.Path.exists", side_effect=[False, True]),
195
- patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
196
- patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
197
- ):
198
- servers, error = list_cursor_servers()
199
- assert error is None
200
- assert servers == ["winserver"]
201
-
202
- def test_list_cursor_servers_read_error(self) -> None:
203
- """Test listing with file read error."""
204
- with (
205
- patch("pathlib.Path.exists", return_value=True),
206
- patch("builtins.open", side_effect=PermissionError("Access denied")),
207
- ):
208
- servers, error = list_cursor_servers()
209
- assert servers is None
210
- assert error is not None
211
- assert "Error reading config" in error
212
- assert "Access denied" in error
213
-
214
-
215
- class TestGetCursorConfigPath:
216
- """Test getting Cursor config path."""
217
-
218
- def test_get_cursor_config_path_unix(self) -> None:
219
- """Test getting config path on Unix-like systems."""
220
- with (
221
- patch("pathlib.Path.home", return_value=Path("/home/user")),
222
- patch("pathlib.Path.exists", return_value=True),
223
- ):
224
- path = get_cursor_config_path()
225
- assert str(path) == str(Path("/home/user/.cursor/mcp.json"))
226
-
227
- def test_get_cursor_config_path_windows(self) -> None:
228
- """Test getting config path on Windows."""
229
- with (
230
- patch("pathlib.Path.home", return_value=Path("/home/user")),
231
- patch("pathlib.Path.exists", return_value=False),
232
- patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
233
- ):
234
- path = get_cursor_config_path()
235
- assert "Test" in str(path)
236
- assert ".cursor" in str(path)
237
- assert "mcp.json" in str(path)
238
-
239
- def test_get_cursor_config_path_no_userprofile(self) -> None:
240
- """Test getting config path when USERPROFILE not set."""
241
- with (
242
- patch("pathlib.Path.home", return_value=Path("/home/user")),
243
- patch("pathlib.Path.exists", return_value=False),
244
- patch.dict(os.environ, {}, clear=True),
245
- ):
246
- path = get_cursor_config_path()
247
- # Should still return something based on empty USERPROFILE
248
- assert ".cursor" in str(path)
249
- assert "mcp.json" in str(path)
250
-
251
-
252
- if __name__ == "__main__":
253
- pytest.main([__file__])
1
+ """Tests for hud.cli.cursor module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from unittest.mock import mock_open, patch
9
+
10
+ import pytest
11
+
12
+ from hud.cli.cursor import get_cursor_config_path, list_cursor_servers, parse_cursor_config
13
+
14
+
15
+ class TestParseCursorConfig:
16
+ """Test Cursor config parsing."""
17
+
18
+ def test_parse_cursor_config_success(self) -> None:
19
+ """Test successful parsing of Cursor config."""
20
+ config_data = {
21
+ "mcpServers": {
22
+ "test-server": {
23
+ "command": "python",
24
+ "args": ["server.py", "--port", "8080"],
25
+ "env": {"KEY": "value"},
26
+ }
27
+ }
28
+ }
29
+
30
+ with (
31
+ patch("pathlib.Path.exists", return_value=True),
32
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
33
+ ):
34
+ command, error = parse_cursor_config("test-server")
35
+ assert error is None
36
+ assert command == ["python", "server.py", "--port", "8080"]
37
+
38
+ def test_parse_cursor_config_not_found(self) -> None:
39
+ """Test parsing when config file doesn't exist."""
40
+ with patch("pathlib.Path.exists", return_value=False):
41
+ command, error = parse_cursor_config("test-server")
42
+ assert command is None
43
+ assert error is not None
44
+ assert "Cursor config not found" in error
45
+
46
+ def test_parse_cursor_config_server_not_found(self) -> None:
47
+ """Test parsing when server doesn't exist in config."""
48
+ config_data = {"mcpServers": {"other-server": {"command": "node", "args": ["server.js"]}}}
49
+
50
+ with (
51
+ patch("pathlib.Path.exists", return_value=True),
52
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
53
+ ):
54
+ command, error = parse_cursor_config("test-server")
55
+ assert command is None
56
+ assert error is not None
57
+ assert "Server 'test-server' not found" in error
58
+ assert "Available: other-server" in error
59
+
60
+ def test_parse_cursor_config_reloaderoo(self) -> None:
61
+ """Test parsing config with reloaderoo wrapper."""
62
+ config_data = {
63
+ "mcpServers": {
64
+ "test-server": {
65
+ "command": "npx",
66
+ "args": ["reloaderoo", "--watch", "src", "--", "python", "server.py"],
67
+ }
68
+ }
69
+ }
70
+
71
+ with (
72
+ patch("pathlib.Path.exists", return_value=True),
73
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
74
+ ):
75
+ command, error = parse_cursor_config("test-server")
76
+ assert error is None
77
+ # Should extract command after --
78
+ assert command == ["python", "server.py"]
79
+
80
+ def test_parse_cursor_config_reloaderoo_no_dash(self) -> None:
81
+ """Test parsing reloaderoo without -- separator."""
82
+ config_data = {
83
+ "mcpServers": {
84
+ "test-server": {"command": "npx", "args": ["reloaderoo", "python", "server.py"]}
85
+ }
86
+ }
87
+
88
+ with (
89
+ patch("pathlib.Path.exists", return_value=True),
90
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
91
+ ):
92
+ command, error = parse_cursor_config("test-server")
93
+ assert error is None
94
+ # Should return full command since no -- found
95
+ assert command == ["npx", "reloaderoo", "python", "server.py"]
96
+
97
+ def test_parse_cursor_config_windows_path(self) -> None:
98
+ """Test parsing with Windows user profile path."""
99
+ config_data = {"mcpServers": {"test": {"command": "cmd"}}}
100
+
101
+ # First path doesn't exist, try Windows path
102
+ with (
103
+ patch("pathlib.Path.exists", side_effect=[False, True]),
104
+ patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
105
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
106
+ ):
107
+ command, error = parse_cursor_config("test")
108
+ assert error is None
109
+ assert command == ["cmd"]
110
+
111
+ def test_parse_cursor_config_json_error(self) -> None:
112
+ """Test parsing with invalid JSON."""
113
+ with (
114
+ patch("pathlib.Path.exists", return_value=True),
115
+ patch("builtins.open", mock_open(read_data="invalid json")),
116
+ ):
117
+ command, error = parse_cursor_config("test-server")
118
+ assert command is None
119
+ assert error is not None
120
+ assert "Error reading config" in error
121
+
122
+ def test_parse_cursor_config_no_command(self) -> None:
123
+ """Test parsing server with no command."""
124
+ config_data = {"mcpServers": {"test-server": {"args": ["--port", "8080"]}}}
125
+
126
+ with (
127
+ patch("pathlib.Path.exists", return_value=True),
128
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
129
+ ):
130
+ command, error = parse_cursor_config("test-server")
131
+ assert error is None
132
+ assert command == ["", "--port", "8080"] # Empty command
133
+
134
+
135
+ class TestListCursorServers:
136
+ """Test listing Cursor servers."""
137
+
138
+ def test_list_cursor_servers_success(self) -> None:
139
+ """Test successful listing of servers."""
140
+ config_data = {
141
+ "mcpServers": {
142
+ "server1": {"command": "python"},
143
+ "server2": {"command": "node"},
144
+ "server3": {"command": "ruby"},
145
+ }
146
+ }
147
+
148
+ with (
149
+ patch("pathlib.Path.exists", return_value=True),
150
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
151
+ ):
152
+ servers, error = list_cursor_servers()
153
+ assert error is None
154
+ assert servers == ["server1", "server2", "server3"]
155
+
156
+ def test_list_cursor_servers_empty(self) -> None:
157
+ """Test listing when no servers configured."""
158
+ config_data = {"mcpServers": {}}
159
+
160
+ with (
161
+ patch("pathlib.Path.exists", return_value=True),
162
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
163
+ ):
164
+ servers, error = list_cursor_servers()
165
+ assert error is None
166
+ assert servers == []
167
+
168
+ def test_list_cursor_servers_no_mcp_section(self) -> None:
169
+ """Test listing when mcpServers section missing."""
170
+ config_data = {"otherConfig": {}}
171
+
172
+ with (
173
+ patch("pathlib.Path.exists", return_value=True),
174
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
175
+ ):
176
+ servers, error = list_cursor_servers()
177
+ assert error is None
178
+ assert servers == []
179
+
180
+ def test_list_cursor_servers_file_not_found(self) -> None:
181
+ """Test listing when config file doesn't exist."""
182
+ with patch("pathlib.Path.exists", return_value=False):
183
+ servers, error = list_cursor_servers()
184
+ assert servers is None
185
+ assert error is not None
186
+ assert "Cursor config not found" in error
187
+
188
+ def test_list_cursor_servers_windows_path(self) -> None:
189
+ """Test listing with Windows path fallback."""
190
+ config_data = {"mcpServers": {"winserver": {"command": "cmd"}}}
191
+
192
+ # First path doesn't exist, second (Windows) does
193
+ with (
194
+ patch("pathlib.Path.exists", side_effect=[False, True]),
195
+ patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
196
+ patch("builtins.open", mock_open(read_data=json.dumps(config_data))),
197
+ ):
198
+ servers, error = list_cursor_servers()
199
+ assert error is None
200
+ assert servers == ["winserver"]
201
+
202
+ def test_list_cursor_servers_read_error(self) -> None:
203
+ """Test listing with file read error."""
204
+ with (
205
+ patch("pathlib.Path.exists", return_value=True),
206
+ patch("builtins.open", side_effect=PermissionError("Access denied")),
207
+ ):
208
+ servers, error = list_cursor_servers()
209
+ assert servers is None
210
+ assert error is not None
211
+ assert "Error reading config" in error
212
+ assert "Access denied" in error
213
+
214
+
215
+ class TestGetCursorConfigPath:
216
+ """Test getting Cursor config path."""
217
+
218
+ def test_get_cursor_config_path_unix(self) -> None:
219
+ """Test getting config path on Unix-like systems."""
220
+ with (
221
+ patch("pathlib.Path.home", return_value=Path("/home/user")),
222
+ patch("pathlib.Path.exists", return_value=True),
223
+ ):
224
+ path = get_cursor_config_path()
225
+ assert str(path) == str(Path("/home/user/.cursor/mcp.json"))
226
+
227
+ def test_get_cursor_config_path_windows(self) -> None:
228
+ """Test getting config path on Windows."""
229
+ with (
230
+ patch("pathlib.Path.home", return_value=Path("/home/user")),
231
+ patch("pathlib.Path.exists", return_value=False),
232
+ patch.dict(os.environ, {"USERPROFILE": "C:\\Users\\Test"}),
233
+ ):
234
+ path = get_cursor_config_path()
235
+ assert "Test" in str(path)
236
+ assert ".cursor" in str(path)
237
+ assert "mcp.json" in str(path)
238
+
239
+ def test_get_cursor_config_path_no_userprofile(self) -> None:
240
+ """Test getting config path when USERPROFILE not set."""
241
+ with (
242
+ patch("pathlib.Path.home", return_value=Path("/home/user")),
243
+ patch("pathlib.Path.exists", return_value=False),
244
+ patch.dict(os.environ, {}, clear=True),
245
+ ):
246
+ path = get_cursor_config_path()
247
+ # Should still return something based on empty USERPROFILE
248
+ assert ".cursor" in str(path)
249
+ assert "mcp.json" in str(path)
250
+
251
+
252
+ if __name__ == "__main__":
253
+ pytest.main([__file__])