hatch-xclam 0.7.0__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.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for MCP CLI partial configuration update functionality.
|
|
3
|
+
|
|
4
|
+
This module tests the partial configuration update feature that allows users to modify
|
|
5
|
+
specific fields without re-specifying entire server configurations.
|
|
6
|
+
|
|
7
|
+
Tests cover:
|
|
8
|
+
- Server existence detection (get_server_config method)
|
|
9
|
+
- Partial update validation (create vs. update logic)
|
|
10
|
+
- Field preservation (merge logic)
|
|
11
|
+
- Command/URL switching behavior
|
|
12
|
+
- End-to-end integration workflows
|
|
13
|
+
- Backward compatibility
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import unittest
|
|
17
|
+
from unittest.mock import patch, MagicMock, call
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
# Add the parent directory to the path to import hatch modules
|
|
22
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
23
|
+
|
|
24
|
+
from hatch.mcp_host_config.host_management import MCPHostConfigurationManager
|
|
25
|
+
from hatch.mcp_host_config.models import MCPHostType, MCPServerConfig, MCPServerConfigOmni
|
|
26
|
+
from hatch.cli_hatch import handle_mcp_configure
|
|
27
|
+
from wobble import regression_test, integration_test
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestServerExistenceDetection(unittest.TestCase):
|
|
31
|
+
"""Test suite for server existence detection (Category A)."""
|
|
32
|
+
|
|
33
|
+
@regression_test
|
|
34
|
+
def test_get_server_config_exists(self):
|
|
35
|
+
"""Test A1: get_server_config returns existing server configuration."""
|
|
36
|
+
# Setup: Create a test server configuration
|
|
37
|
+
manager = MCPHostConfigurationManager()
|
|
38
|
+
|
|
39
|
+
# Mock the strategy to return a configuration with our test server
|
|
40
|
+
mock_strategy = MagicMock()
|
|
41
|
+
mock_config = MagicMock()
|
|
42
|
+
test_server = MCPServerConfig(
|
|
43
|
+
name="test-server",
|
|
44
|
+
command="python",
|
|
45
|
+
args=["server.py"],
|
|
46
|
+
env={"API_KEY": "test_key"}
|
|
47
|
+
)
|
|
48
|
+
mock_config.servers = {"test-server": test_server}
|
|
49
|
+
mock_strategy.read_configuration.return_value = mock_config
|
|
50
|
+
|
|
51
|
+
with patch.object(manager.host_registry, 'get_strategy', return_value=mock_strategy):
|
|
52
|
+
# Execute
|
|
53
|
+
result = manager.get_server_config("claude-desktop", "test-server")
|
|
54
|
+
|
|
55
|
+
# Validate
|
|
56
|
+
self.assertIsNotNone(result)
|
|
57
|
+
self.assertEqual(result.name, "test-server")
|
|
58
|
+
self.assertEqual(result.command, "python")
|
|
59
|
+
|
|
60
|
+
@regression_test
|
|
61
|
+
def test_get_server_config_not_exists(self):
|
|
62
|
+
"""Test A2: get_server_config returns None for non-existent server."""
|
|
63
|
+
# Setup: Empty registry
|
|
64
|
+
manager = MCPHostConfigurationManager()
|
|
65
|
+
|
|
66
|
+
mock_strategy = MagicMock()
|
|
67
|
+
mock_config = MagicMock()
|
|
68
|
+
mock_config.servers = {} # No servers
|
|
69
|
+
mock_strategy.read_configuration.return_value = mock_config
|
|
70
|
+
|
|
71
|
+
with patch.object(manager.host_registry, 'get_strategy', return_value=mock_strategy):
|
|
72
|
+
# Execute
|
|
73
|
+
result = manager.get_server_config("claude-desktop", "non-existent-server")
|
|
74
|
+
|
|
75
|
+
# Validate
|
|
76
|
+
self.assertIsNone(result)
|
|
77
|
+
|
|
78
|
+
@regression_test
|
|
79
|
+
def test_get_server_config_invalid_host(self):
|
|
80
|
+
"""Test A3: get_server_config handles invalid host gracefully."""
|
|
81
|
+
# Setup
|
|
82
|
+
manager = MCPHostConfigurationManager()
|
|
83
|
+
|
|
84
|
+
# Execute: Invalid host should be handled gracefully
|
|
85
|
+
result = manager.get_server_config("invalid-host", "test-server")
|
|
86
|
+
|
|
87
|
+
# Validate: Should return None, not raise exception
|
|
88
|
+
self.assertIsNone(result)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestPartialUpdateValidation(unittest.TestCase):
|
|
92
|
+
"""Test suite for partial update validation (Category B)."""
|
|
93
|
+
|
|
94
|
+
@regression_test
|
|
95
|
+
def test_configure_update_single_field_timeout(self):
|
|
96
|
+
"""Test B1: Update single field (timeout) preserves other fields."""
|
|
97
|
+
# Setup: Existing server with timeout=30
|
|
98
|
+
existing_server = MCPServerConfig(
|
|
99
|
+
name="test-server",
|
|
100
|
+
command="python",
|
|
101
|
+
args=["server.py"],
|
|
102
|
+
env={"API_KEY": "test_key"},
|
|
103
|
+
timeout=30
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
107
|
+
mock_manager = MagicMock()
|
|
108
|
+
mock_manager_class.return_value = mock_manager
|
|
109
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
110
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
111
|
+
|
|
112
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
113
|
+
# Execute: Update only timeout (use Gemini which supports timeout)
|
|
114
|
+
result = handle_mcp_configure(
|
|
115
|
+
host="gemini",
|
|
116
|
+
server_name="test-server",
|
|
117
|
+
command=None,
|
|
118
|
+
args=None,
|
|
119
|
+
env=None,
|
|
120
|
+
url=None,
|
|
121
|
+
header=None,
|
|
122
|
+
timeout=60, # Only timeout provided
|
|
123
|
+
trust=False,
|
|
124
|
+
cwd=None,
|
|
125
|
+
env_file=None,
|
|
126
|
+
http_url=None,
|
|
127
|
+
include_tools=None,
|
|
128
|
+
exclude_tools=None,
|
|
129
|
+
input=None,
|
|
130
|
+
no_backup=False,
|
|
131
|
+
dry_run=False,
|
|
132
|
+
auto_approve=True
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Validate: Should succeed
|
|
136
|
+
self.assertEqual(result, 0)
|
|
137
|
+
|
|
138
|
+
# Validate: configure_server was called with merged config
|
|
139
|
+
mock_manager.configure_server.assert_called_once()
|
|
140
|
+
call_args = mock_manager.configure_server.call_args
|
|
141
|
+
host_config = call_args[1]['server_config']
|
|
142
|
+
|
|
143
|
+
# Timeout should be updated (Gemini supports timeout)
|
|
144
|
+
self.assertEqual(host_config.timeout, 60)
|
|
145
|
+
# Other fields should be preserved
|
|
146
|
+
self.assertEqual(host_config.command, "python")
|
|
147
|
+
self.assertEqual(host_config.args, ["server.py"])
|
|
148
|
+
|
|
149
|
+
@regression_test
|
|
150
|
+
def test_configure_update_env_vars_only(self):
|
|
151
|
+
"""Test B2: Update environment variables only preserves other fields."""
|
|
152
|
+
# Setup: Existing server with env vars
|
|
153
|
+
existing_server = MCPServerConfig(
|
|
154
|
+
name="test-server",
|
|
155
|
+
command="python",
|
|
156
|
+
args=["server.py"],
|
|
157
|
+
env={"API_KEY": "old_key"}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
161
|
+
mock_manager = MagicMock()
|
|
162
|
+
mock_manager_class.return_value = mock_manager
|
|
163
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
164
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
165
|
+
|
|
166
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
167
|
+
# Execute: Update only env vars
|
|
168
|
+
result = handle_mcp_configure(
|
|
169
|
+
host="claude-desktop",
|
|
170
|
+
server_name="test-server",
|
|
171
|
+
command=None,
|
|
172
|
+
args=None,
|
|
173
|
+
env=["NEW_KEY=new_value"], # Only env provided
|
|
174
|
+
url=None,
|
|
175
|
+
header=None,
|
|
176
|
+
timeout=None,
|
|
177
|
+
trust=False,
|
|
178
|
+
cwd=None,
|
|
179
|
+
env_file=None,
|
|
180
|
+
http_url=None,
|
|
181
|
+
include_tools=None,
|
|
182
|
+
exclude_tools=None,
|
|
183
|
+
input=None,
|
|
184
|
+
no_backup=False,
|
|
185
|
+
dry_run=False,
|
|
186
|
+
auto_approve=True
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Validate: Should succeed
|
|
190
|
+
self.assertEqual(result, 0)
|
|
191
|
+
|
|
192
|
+
# Validate: configure_server was called with merged config
|
|
193
|
+
mock_manager.configure_server.assert_called_once()
|
|
194
|
+
call_args = mock_manager.configure_server.call_args
|
|
195
|
+
omni_config = call_args[1]['server_config']
|
|
196
|
+
|
|
197
|
+
# Env should be updated
|
|
198
|
+
self.assertEqual(omni_config.env, {"NEW_KEY": "new_value"})
|
|
199
|
+
# Other fields should be preserved
|
|
200
|
+
self.assertEqual(omni_config.command, "python")
|
|
201
|
+
self.assertEqual(omni_config.args, ["server.py"])
|
|
202
|
+
|
|
203
|
+
@regression_test
|
|
204
|
+
def test_configure_create_requires_command_or_url(self):
|
|
205
|
+
"""Test B4: Create operation requires command or url."""
|
|
206
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
207
|
+
mock_manager = MagicMock()
|
|
208
|
+
mock_manager_class.return_value = mock_manager
|
|
209
|
+
mock_manager.get_server_config.return_value = None # Server doesn't exist
|
|
210
|
+
|
|
211
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
212
|
+
# Execute: Create without command or url
|
|
213
|
+
result = handle_mcp_configure(
|
|
214
|
+
host="claude-desktop",
|
|
215
|
+
server_name="new-server",
|
|
216
|
+
command=None, # No command
|
|
217
|
+
args=None,
|
|
218
|
+
env=None,
|
|
219
|
+
url=None, # No url
|
|
220
|
+
header=None,
|
|
221
|
+
timeout=60,
|
|
222
|
+
trust=False,
|
|
223
|
+
cwd=None,
|
|
224
|
+
env_file=None,
|
|
225
|
+
http_url=None,
|
|
226
|
+
include_tools=None,
|
|
227
|
+
exclude_tools=None,
|
|
228
|
+
input=None,
|
|
229
|
+
no_backup=False,
|
|
230
|
+
dry_run=False,
|
|
231
|
+
auto_approve=True
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Validate: Should fail with error
|
|
235
|
+
self.assertEqual(result, 1)
|
|
236
|
+
|
|
237
|
+
# Validate: Error message mentions command or url
|
|
238
|
+
mock_print.assert_called()
|
|
239
|
+
error_message = str(mock_print.call_args[0][0])
|
|
240
|
+
self.assertIn("command", error_message.lower())
|
|
241
|
+
self.assertIn("url", error_message.lower())
|
|
242
|
+
|
|
243
|
+
@regression_test
|
|
244
|
+
def test_configure_update_allows_no_command_url(self):
|
|
245
|
+
"""Test B5: Update operation allows omitting command/url."""
|
|
246
|
+
# Setup: Existing server with command
|
|
247
|
+
existing_server = MCPServerConfig(
|
|
248
|
+
name="test-server",
|
|
249
|
+
command="python",
|
|
250
|
+
args=["server.py"]
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
254
|
+
mock_manager = MagicMock()
|
|
255
|
+
mock_manager_class.return_value = mock_manager
|
|
256
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
257
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
258
|
+
|
|
259
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
260
|
+
# Execute: Update without command or url
|
|
261
|
+
result = handle_mcp_configure(
|
|
262
|
+
host="claude-desktop",
|
|
263
|
+
server_name="test-server",
|
|
264
|
+
command=None, # No command
|
|
265
|
+
args=None,
|
|
266
|
+
env=None,
|
|
267
|
+
url=None, # No url
|
|
268
|
+
header=None,
|
|
269
|
+
timeout=60, # Only timeout
|
|
270
|
+
trust=False,
|
|
271
|
+
cwd=None,
|
|
272
|
+
env_file=None,
|
|
273
|
+
http_url=None,
|
|
274
|
+
include_tools=None,
|
|
275
|
+
exclude_tools=None,
|
|
276
|
+
input=None,
|
|
277
|
+
no_backup=False,
|
|
278
|
+
dry_run=False,
|
|
279
|
+
auto_approve=True
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Validate: Should succeed
|
|
283
|
+
self.assertEqual(result, 0)
|
|
284
|
+
|
|
285
|
+
# Validate: Command should be preserved
|
|
286
|
+
mock_manager.configure_server.assert_called_once()
|
|
287
|
+
call_args = mock_manager.configure_server.call_args
|
|
288
|
+
omni_config = call_args[1]['server_config']
|
|
289
|
+
self.assertEqual(omni_config.command, "python")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class TestFieldPreservation(unittest.TestCase):
|
|
293
|
+
"""Test suite for field preservation verification (Category C)."""
|
|
294
|
+
|
|
295
|
+
@regression_test
|
|
296
|
+
def test_configure_update_preserves_unspecified_fields(self):
|
|
297
|
+
"""Test C1: Unspecified fields remain unchanged during update."""
|
|
298
|
+
# Setup: Existing server with multiple fields
|
|
299
|
+
existing_server = MCPServerConfig(
|
|
300
|
+
name="test-server",
|
|
301
|
+
command="python",
|
|
302
|
+
args=["server.py"],
|
|
303
|
+
env={"API_KEY": "test_key"},
|
|
304
|
+
timeout=30
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
308
|
+
mock_manager = MagicMock()
|
|
309
|
+
mock_manager_class.return_value = mock_manager
|
|
310
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
311
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
312
|
+
|
|
313
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
314
|
+
# Execute: Update only timeout (use Gemini which supports timeout)
|
|
315
|
+
result = handle_mcp_configure(
|
|
316
|
+
host="gemini",
|
|
317
|
+
server_name="test-server",
|
|
318
|
+
command=None,
|
|
319
|
+
args=None,
|
|
320
|
+
env=None,
|
|
321
|
+
url=None,
|
|
322
|
+
header=None,
|
|
323
|
+
timeout=60, # Only timeout updated
|
|
324
|
+
trust=False,
|
|
325
|
+
cwd=None,
|
|
326
|
+
env_file=None,
|
|
327
|
+
http_url=None,
|
|
328
|
+
include_tools=None,
|
|
329
|
+
exclude_tools=None,
|
|
330
|
+
input=None,
|
|
331
|
+
no_backup=False,
|
|
332
|
+
dry_run=False,
|
|
333
|
+
auto_approve=True
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Validate
|
|
337
|
+
self.assertEqual(result, 0)
|
|
338
|
+
call_args = mock_manager.configure_server.call_args
|
|
339
|
+
host_config = call_args[1]['server_config']
|
|
340
|
+
|
|
341
|
+
# Timeout updated (Gemini supports timeout)
|
|
342
|
+
self.assertEqual(host_config.timeout, 60)
|
|
343
|
+
# All other fields preserved
|
|
344
|
+
self.assertEqual(host_config.command, "python")
|
|
345
|
+
self.assertEqual(host_config.args, ["server.py"])
|
|
346
|
+
self.assertEqual(host_config.env, {"API_KEY": "test_key"})
|
|
347
|
+
|
|
348
|
+
@regression_test
|
|
349
|
+
def test_configure_update_dependent_fields(self):
|
|
350
|
+
"""Test C3+C4: Update dependent fields without parent field."""
|
|
351
|
+
# Scenario 1: Update args without command
|
|
352
|
+
existing_cmd_server = MCPServerConfig(
|
|
353
|
+
name="cmd-server",
|
|
354
|
+
command="python",
|
|
355
|
+
args=["old.py"]
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
359
|
+
mock_manager = MagicMock()
|
|
360
|
+
mock_manager_class.return_value = mock_manager
|
|
361
|
+
mock_manager.get_server_config.return_value = existing_cmd_server
|
|
362
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
363
|
+
|
|
364
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
365
|
+
# Execute: Update args without command
|
|
366
|
+
result = handle_mcp_configure(
|
|
367
|
+
host="claude-desktop",
|
|
368
|
+
server_name="cmd-server",
|
|
369
|
+
command=None, # Command not provided
|
|
370
|
+
args=["new.py"], # Args updated
|
|
371
|
+
env=None,
|
|
372
|
+
url=None,
|
|
373
|
+
header=None,
|
|
374
|
+
timeout=None,
|
|
375
|
+
trust=False,
|
|
376
|
+
cwd=None,
|
|
377
|
+
env_file=None,
|
|
378
|
+
http_url=None,
|
|
379
|
+
include_tools=None,
|
|
380
|
+
exclude_tools=None,
|
|
381
|
+
input=None,
|
|
382
|
+
no_backup=False,
|
|
383
|
+
dry_run=False,
|
|
384
|
+
auto_approve=True
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Validate: Should succeed
|
|
388
|
+
self.assertEqual(result, 0)
|
|
389
|
+
call_args = mock_manager.configure_server.call_args
|
|
390
|
+
omni_config = call_args[1]['server_config']
|
|
391
|
+
|
|
392
|
+
# Args updated, command preserved
|
|
393
|
+
self.assertEqual(omni_config.args, ["new.py"])
|
|
394
|
+
self.assertEqual(omni_config.command, "python")
|
|
395
|
+
|
|
396
|
+
# Scenario 2: Update headers without url
|
|
397
|
+
existing_url_server = MCPServerConfig(
|
|
398
|
+
name="url-server",
|
|
399
|
+
url="http://localhost:8080",
|
|
400
|
+
headers={"Authorization": "Bearer old_token"}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
404
|
+
mock_manager = MagicMock()
|
|
405
|
+
mock_manager_class.return_value = mock_manager
|
|
406
|
+
mock_manager.get_server_config.return_value = existing_url_server
|
|
407
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
408
|
+
|
|
409
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
410
|
+
# Execute: Update headers without url
|
|
411
|
+
result = handle_mcp_configure(
|
|
412
|
+
host="claude-desktop",
|
|
413
|
+
server_name="url-server",
|
|
414
|
+
command=None,
|
|
415
|
+
args=None,
|
|
416
|
+
env=None,
|
|
417
|
+
url=None, # URL not provided
|
|
418
|
+
header=["Authorization=Bearer new_token"], # Headers updated
|
|
419
|
+
timeout=None,
|
|
420
|
+
trust=False,
|
|
421
|
+
cwd=None,
|
|
422
|
+
env_file=None,
|
|
423
|
+
http_url=None,
|
|
424
|
+
include_tools=None,
|
|
425
|
+
exclude_tools=None,
|
|
426
|
+
input=None,
|
|
427
|
+
no_backup=False,
|
|
428
|
+
dry_run=False,
|
|
429
|
+
auto_approve=True
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Validate: Should succeed
|
|
433
|
+
self.assertEqual(result, 0)
|
|
434
|
+
call_args = mock_manager.configure_server.call_args
|
|
435
|
+
omni_config = call_args[1]['server_config']
|
|
436
|
+
|
|
437
|
+
# Headers updated, url preserved
|
|
438
|
+
self.assertEqual(omni_config.headers, {"Authorization": "Bearer new_token"})
|
|
439
|
+
self.assertEqual(omni_config.url, "http://localhost:8080")
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class TestCommandUrlSwitching(unittest.TestCase):
|
|
443
|
+
"""Test suite for command/URL switching behavior (Category E) [CRITICAL]."""
|
|
444
|
+
|
|
445
|
+
@regression_test
|
|
446
|
+
def test_configure_switch_command_to_url(self):
|
|
447
|
+
"""Test E1: Switch from command-based to URL-based server [CRITICAL]."""
|
|
448
|
+
# Setup: Existing command-based server
|
|
449
|
+
existing_server = MCPServerConfig(
|
|
450
|
+
name="test-server",
|
|
451
|
+
command="python",
|
|
452
|
+
args=["server.py"],
|
|
453
|
+
env={"API_KEY": "test_key"}
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
457
|
+
mock_manager = MagicMock()
|
|
458
|
+
mock_manager_class.return_value = mock_manager
|
|
459
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
460
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
461
|
+
|
|
462
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
463
|
+
# Execute: Switch to URL-based (use gemini which supports URL)
|
|
464
|
+
result = handle_mcp_configure(
|
|
465
|
+
host="gemini",
|
|
466
|
+
server_name="test-server",
|
|
467
|
+
command=None,
|
|
468
|
+
args=None,
|
|
469
|
+
env=None,
|
|
470
|
+
url="http://localhost:8080", # Provide URL
|
|
471
|
+
header=["Authorization=Bearer token"], # Provide headers
|
|
472
|
+
timeout=None,
|
|
473
|
+
trust=False,
|
|
474
|
+
cwd=None,
|
|
475
|
+
env_file=None,
|
|
476
|
+
http_url=None,
|
|
477
|
+
include_tools=None,
|
|
478
|
+
exclude_tools=None,
|
|
479
|
+
input=None,
|
|
480
|
+
no_backup=False,
|
|
481
|
+
dry_run=False,
|
|
482
|
+
auto_approve=True
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Validate: Should succeed
|
|
486
|
+
self.assertEqual(result, 0)
|
|
487
|
+
call_args = mock_manager.configure_server.call_args
|
|
488
|
+
omni_config = call_args[1]['server_config']
|
|
489
|
+
|
|
490
|
+
# URL-based fields set
|
|
491
|
+
self.assertEqual(omni_config.url, "http://localhost:8080")
|
|
492
|
+
self.assertEqual(omni_config.headers, {"Authorization": "Bearer token"})
|
|
493
|
+
# Command-based fields cleared
|
|
494
|
+
self.assertIsNone(omni_config.command)
|
|
495
|
+
self.assertIsNone(omni_config.args)
|
|
496
|
+
# Type field updated to 'sse' (Issue 1)
|
|
497
|
+
self.assertEqual(omni_config.type, "sse")
|
|
498
|
+
|
|
499
|
+
@regression_test
|
|
500
|
+
def test_configure_switch_url_to_command(self):
|
|
501
|
+
"""Test E2: Switch from URL-based to command-based server [CRITICAL]."""
|
|
502
|
+
# Setup: Existing URL-based server
|
|
503
|
+
existing_server = MCPServerConfig(
|
|
504
|
+
name="test-server",
|
|
505
|
+
url="http://localhost:8080",
|
|
506
|
+
headers={"Authorization": "Bearer token"}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
510
|
+
mock_manager = MagicMock()
|
|
511
|
+
mock_manager_class.return_value = mock_manager
|
|
512
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
513
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
514
|
+
|
|
515
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
516
|
+
# Execute: Switch to command-based (use gemini which supports both)
|
|
517
|
+
result = handle_mcp_configure(
|
|
518
|
+
host="gemini",
|
|
519
|
+
server_name="test-server",
|
|
520
|
+
command="node", # Provide command
|
|
521
|
+
args=["server.js"], # Provide args
|
|
522
|
+
env=None,
|
|
523
|
+
url=None,
|
|
524
|
+
header=None,
|
|
525
|
+
timeout=None,
|
|
526
|
+
trust=False,
|
|
527
|
+
cwd=None,
|
|
528
|
+
env_file=None,
|
|
529
|
+
http_url=None,
|
|
530
|
+
include_tools=None,
|
|
531
|
+
exclude_tools=None,
|
|
532
|
+
input=None,
|
|
533
|
+
no_backup=False,
|
|
534
|
+
dry_run=False,
|
|
535
|
+
auto_approve=True
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Validate: Should succeed
|
|
539
|
+
self.assertEqual(result, 0)
|
|
540
|
+
call_args = mock_manager.configure_server.call_args
|
|
541
|
+
omni_config = call_args[1]['server_config']
|
|
542
|
+
|
|
543
|
+
# Command-based fields set
|
|
544
|
+
self.assertEqual(omni_config.command, "node")
|
|
545
|
+
self.assertEqual(omni_config.args, ["server.js"])
|
|
546
|
+
# URL-based fields cleared
|
|
547
|
+
self.assertIsNone(omni_config.url)
|
|
548
|
+
self.assertIsNone(omni_config.headers)
|
|
549
|
+
# Type field updated to 'stdio' (Issue 1)
|
|
550
|
+
self.assertEqual(omni_config.type, "stdio")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class TestPartialUpdateIntegration(unittest.TestCase):
|
|
554
|
+
"""Test suite for end-to-end partial update workflows (Integration Tests)."""
|
|
555
|
+
|
|
556
|
+
@integration_test(scope="component")
|
|
557
|
+
def test_partial_update_end_to_end_timeout(self):
|
|
558
|
+
"""Test I1: End-to-end partial update workflow for timeout field."""
|
|
559
|
+
# Setup: Existing server
|
|
560
|
+
existing_server = MCPServerConfig(
|
|
561
|
+
name="test-server",
|
|
562
|
+
command="python",
|
|
563
|
+
args=["server.py"],
|
|
564
|
+
timeout=30
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
568
|
+
mock_manager = MagicMock()
|
|
569
|
+
mock_manager_class.return_value = mock_manager
|
|
570
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
571
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
572
|
+
|
|
573
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
574
|
+
with patch('hatch.cli_hatch.generate_conversion_report') as mock_report:
|
|
575
|
+
# Mock report to verify UNCHANGED detection
|
|
576
|
+
mock_report.return_value = MagicMock()
|
|
577
|
+
|
|
578
|
+
# Execute: Full CLI workflow
|
|
579
|
+
result = handle_mcp_configure(
|
|
580
|
+
host="claude-desktop",
|
|
581
|
+
server_name="test-server",
|
|
582
|
+
command=None,
|
|
583
|
+
args=None,
|
|
584
|
+
env=None,
|
|
585
|
+
url=None,
|
|
586
|
+
header=None,
|
|
587
|
+
timeout=60, # Update timeout only
|
|
588
|
+
trust=False,
|
|
589
|
+
cwd=None,
|
|
590
|
+
env_file=None,
|
|
591
|
+
http_url=None,
|
|
592
|
+
include_tools=None,
|
|
593
|
+
exclude_tools=None,
|
|
594
|
+
input=None,
|
|
595
|
+
no_backup=False,
|
|
596
|
+
dry_run=False,
|
|
597
|
+
auto_approve=True
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Validate: Should succeed
|
|
601
|
+
self.assertEqual(result, 0)
|
|
602
|
+
|
|
603
|
+
# Validate: Report was generated with old_config for UNCHANGED detection
|
|
604
|
+
mock_report.assert_called_once()
|
|
605
|
+
call_kwargs = mock_report.call_args[1]
|
|
606
|
+
self.assertEqual(call_kwargs['operation'], 'update')
|
|
607
|
+
self.assertIsNotNone(call_kwargs.get('old_config'))
|
|
608
|
+
|
|
609
|
+
@integration_test(scope="component")
|
|
610
|
+
def test_partial_update_end_to_end_switch_type(self):
|
|
611
|
+
"""Test I2: End-to-end workflow for command/URL switching."""
|
|
612
|
+
# Setup: Existing command-based server
|
|
613
|
+
existing_server = MCPServerConfig(
|
|
614
|
+
name="test-server",
|
|
615
|
+
command="python",
|
|
616
|
+
args=["server.py"]
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
620
|
+
mock_manager = MagicMock()
|
|
621
|
+
mock_manager_class.return_value = mock_manager
|
|
622
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
623
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
624
|
+
|
|
625
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
626
|
+
with patch('hatch.cli_hatch.generate_conversion_report') as mock_report:
|
|
627
|
+
mock_report.return_value = MagicMock()
|
|
628
|
+
|
|
629
|
+
# Execute: Switch to URL-based (use gemini which supports URL)
|
|
630
|
+
result = handle_mcp_configure(
|
|
631
|
+
host="gemini",
|
|
632
|
+
server_name="test-server",
|
|
633
|
+
command=None,
|
|
634
|
+
args=None,
|
|
635
|
+
env=None,
|
|
636
|
+
url="http://localhost:8080",
|
|
637
|
+
header=["Authorization=Bearer token"],
|
|
638
|
+
timeout=None,
|
|
639
|
+
trust=False,
|
|
640
|
+
cwd=None,
|
|
641
|
+
env_file=None,
|
|
642
|
+
http_url=None,
|
|
643
|
+
include_tools=None,
|
|
644
|
+
exclude_tools=None,
|
|
645
|
+
input=None,
|
|
646
|
+
no_backup=False,
|
|
647
|
+
dry_run=False,
|
|
648
|
+
auto_approve=True
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
# Validate: Should succeed
|
|
652
|
+
self.assertEqual(result, 0)
|
|
653
|
+
|
|
654
|
+
# Validate: Server type switched
|
|
655
|
+
call_args = mock_manager.configure_server.call_args
|
|
656
|
+
omni_config = call_args[1]['server_config']
|
|
657
|
+
self.assertEqual(omni_config.url, "http://localhost:8080")
|
|
658
|
+
self.assertIsNone(omni_config.command)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class TestBackwardCompatibility(unittest.TestCase):
|
|
662
|
+
"""Test suite for backward compatibility (Regression Tests)."""
|
|
663
|
+
|
|
664
|
+
@regression_test
|
|
665
|
+
def test_existing_create_operation_unchanged(self):
|
|
666
|
+
"""Test R1: Existing create operations work identically."""
|
|
667
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
668
|
+
mock_manager = MagicMock()
|
|
669
|
+
mock_manager_class.return_value = mock_manager
|
|
670
|
+
mock_manager.get_server_config.return_value = None # Server doesn't exist
|
|
671
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
672
|
+
|
|
673
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
674
|
+
# Execute: Create operation with full configuration (use Gemini for timeout support)
|
|
675
|
+
result = handle_mcp_configure(
|
|
676
|
+
host="gemini",
|
|
677
|
+
server_name="new-server",
|
|
678
|
+
command="python",
|
|
679
|
+
args=["server.py"],
|
|
680
|
+
env=["API_KEY=secret"],
|
|
681
|
+
url=None,
|
|
682
|
+
header=None,
|
|
683
|
+
timeout=30,
|
|
684
|
+
trust=False,
|
|
685
|
+
cwd=None,
|
|
686
|
+
env_file=None,
|
|
687
|
+
http_url=None,
|
|
688
|
+
include_tools=None,
|
|
689
|
+
exclude_tools=None,
|
|
690
|
+
input=None,
|
|
691
|
+
no_backup=False,
|
|
692
|
+
dry_run=False,
|
|
693
|
+
auto_approve=True
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Validate: Should succeed
|
|
697
|
+
self.assertEqual(result, 0)
|
|
698
|
+
|
|
699
|
+
# Validate: Server created with all fields
|
|
700
|
+
mock_manager.configure_server.assert_called_once()
|
|
701
|
+
call_args = mock_manager.configure_server.call_args
|
|
702
|
+
host_config = call_args[1]['server_config']
|
|
703
|
+
self.assertEqual(host_config.command, "python")
|
|
704
|
+
self.assertEqual(host_config.args, ["server.py"])
|
|
705
|
+
self.assertEqual(host_config.timeout, 30)
|
|
706
|
+
|
|
707
|
+
@regression_test
|
|
708
|
+
def test_error_messages_remain_clear(self):
|
|
709
|
+
"""Test R2: Error messages are clear and helpful (modified)."""
|
|
710
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
711
|
+
mock_manager = MagicMock()
|
|
712
|
+
mock_manager_class.return_value = mock_manager
|
|
713
|
+
mock_manager.get_server_config.return_value = None # Server doesn't exist
|
|
714
|
+
|
|
715
|
+
with patch('hatch.cli_hatch.print') as mock_print:
|
|
716
|
+
# Execute: Create without command or url
|
|
717
|
+
result = handle_mcp_configure(
|
|
718
|
+
host="claude-desktop",
|
|
719
|
+
server_name="new-server",
|
|
720
|
+
command=None, # No command
|
|
721
|
+
args=None,
|
|
722
|
+
env=None,
|
|
723
|
+
url=None, # No url
|
|
724
|
+
header=None,
|
|
725
|
+
timeout=60,
|
|
726
|
+
trust=False,
|
|
727
|
+
cwd=None,
|
|
728
|
+
env_file=None,
|
|
729
|
+
http_url=None,
|
|
730
|
+
include_tools=None,
|
|
731
|
+
exclude_tools=None,
|
|
732
|
+
input=None,
|
|
733
|
+
no_backup=False,
|
|
734
|
+
dry_run=False,
|
|
735
|
+
auto_approve=True
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Validate: Should fail
|
|
739
|
+
self.assertEqual(result, 1)
|
|
740
|
+
|
|
741
|
+
# Validate: Error message is clear
|
|
742
|
+
mock_print.assert_called()
|
|
743
|
+
error_message = str(mock_print.call_args[0][0])
|
|
744
|
+
self.assertIn("command", error_message.lower())
|
|
745
|
+
self.assertIn("url", error_message.lower())
|
|
746
|
+
# Should mention this is for creating a new server
|
|
747
|
+
self.assertTrue(
|
|
748
|
+
"creat" in error_message.lower() or "new" in error_message.lower(),
|
|
749
|
+
f"Error message should clarify this is for creating: {error_message}"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class TestTypeFieldUpdating(unittest.TestCase):
|
|
754
|
+
"""Test suite for type field updates during transport switching (Issue 1)."""
|
|
755
|
+
|
|
756
|
+
@regression_test
|
|
757
|
+
def test_type_field_updates_command_to_url(self):
|
|
758
|
+
"""Test type field updates from 'stdio' to 'sse' when switching to URL."""
|
|
759
|
+
# Setup: Create existing command-based server with type='stdio'
|
|
760
|
+
existing_server = MCPServerConfig(
|
|
761
|
+
name="test-server",
|
|
762
|
+
type="stdio",
|
|
763
|
+
command="python",
|
|
764
|
+
args=["server.py"]
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
768
|
+
mock_manager = MagicMock()
|
|
769
|
+
mock_manager_class.return_value = mock_manager
|
|
770
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
771
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
772
|
+
|
|
773
|
+
with patch('hatch.cli_hatch.print'):
|
|
774
|
+
# Execute: Switch to URL-based configuration
|
|
775
|
+
result = handle_mcp_configure(
|
|
776
|
+
host='gemini',
|
|
777
|
+
server_name='test-server',
|
|
778
|
+
command=None,
|
|
779
|
+
args=None,
|
|
780
|
+
env=None,
|
|
781
|
+
url='http://localhost:8080',
|
|
782
|
+
header=None,
|
|
783
|
+
timeout=None,
|
|
784
|
+
trust=False,
|
|
785
|
+
cwd=None,
|
|
786
|
+
env_file=None,
|
|
787
|
+
http_url=None,
|
|
788
|
+
include_tools=None,
|
|
789
|
+
exclude_tools=None,
|
|
790
|
+
input=None,
|
|
791
|
+
no_backup=False,
|
|
792
|
+
dry_run=False,
|
|
793
|
+
auto_approve=True
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Validate: Should succeed
|
|
797
|
+
self.assertEqual(result, 0)
|
|
798
|
+
|
|
799
|
+
# Validate: Type field updated to 'sse'
|
|
800
|
+
call_args = mock_manager.configure_server.call_args
|
|
801
|
+
server_config = call_args.kwargs['server_config']
|
|
802
|
+
self.assertEqual(server_config.type, "sse")
|
|
803
|
+
self.assertIsNone(server_config.command)
|
|
804
|
+
self.assertEqual(server_config.url, "http://localhost:8080")
|
|
805
|
+
|
|
806
|
+
@regression_test
|
|
807
|
+
def test_type_field_updates_url_to_command(self):
|
|
808
|
+
"""Test type field updates from 'sse' to 'stdio' when switching to command."""
|
|
809
|
+
# Setup: Create existing URL-based server with type='sse'
|
|
810
|
+
existing_server = MCPServerConfig(
|
|
811
|
+
name="test-server",
|
|
812
|
+
type="sse",
|
|
813
|
+
url="http://localhost:8080",
|
|
814
|
+
headers={"Authorization": "Bearer token"}
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
818
|
+
mock_manager = MagicMock()
|
|
819
|
+
mock_manager_class.return_value = mock_manager
|
|
820
|
+
mock_manager.get_server_config.return_value = existing_server
|
|
821
|
+
mock_manager.configure_server.return_value = MagicMock(success=True)
|
|
822
|
+
|
|
823
|
+
with patch('hatch.cli_hatch.print'):
|
|
824
|
+
# Execute: Switch to command-based configuration
|
|
825
|
+
result = handle_mcp_configure(
|
|
826
|
+
host='gemini',
|
|
827
|
+
server_name='test-server',
|
|
828
|
+
command='python',
|
|
829
|
+
args=['server.py'],
|
|
830
|
+
env=None,
|
|
831
|
+
url=None,
|
|
832
|
+
header=None,
|
|
833
|
+
timeout=None,
|
|
834
|
+
trust=False,
|
|
835
|
+
cwd=None,
|
|
836
|
+
env_file=None,
|
|
837
|
+
http_url=None,
|
|
838
|
+
include_tools=None,
|
|
839
|
+
exclude_tools=None,
|
|
840
|
+
input=None,
|
|
841
|
+
no_backup=False,
|
|
842
|
+
dry_run=False,
|
|
843
|
+
auto_approve=True
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# Validate: Should succeed
|
|
847
|
+
self.assertEqual(result, 0)
|
|
848
|
+
|
|
849
|
+
# Validate: Type field updated to 'stdio'
|
|
850
|
+
call_args = mock_manager.configure_server.call_args
|
|
851
|
+
server_config = call_args.kwargs['server_config']
|
|
852
|
+
self.assertEqual(server_config.type, "stdio")
|
|
853
|
+
self.assertEqual(server_config.command, "python")
|
|
854
|
+
self.assertIsNone(server_config.url)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
if __name__ == '__main__':
|
|
858
|
+
unittest.main()
|
|
859
|
+
|