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,453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for MCP CLI direct management commands (Phase 3e).
|
|
3
|
+
|
|
4
|
+
This module tests the new MCP direct management functionality:
|
|
5
|
+
- hatch mcp configure
|
|
6
|
+
- hatch mcp remove
|
|
7
|
+
|
|
8
|
+
Tests cover argument parsing, server configuration, output formatting,
|
|
9
|
+
and error handling scenarios.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest.mock import patch, MagicMock, ANY
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Add the parent directory to the path to import hatch modules
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
19
|
+
|
|
20
|
+
from hatch.cli_hatch import (
|
|
21
|
+
main, handle_mcp_configure, handle_mcp_remove, handle_mcp_remove_server,
|
|
22
|
+
handle_mcp_remove_host, parse_env_vars, parse_header
|
|
23
|
+
)
|
|
24
|
+
from hatch.mcp_host_config.models import MCPHostType, MCPServerConfig
|
|
25
|
+
from wobble import regression_test, integration_test
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestMCPConfigureCommand(unittest.TestCase):
|
|
29
|
+
"""Test suite for MCP configure command."""
|
|
30
|
+
|
|
31
|
+
@regression_test
|
|
32
|
+
def test_configure_argument_parsing_basic(self):
|
|
33
|
+
"""Test basic argument parsing for 'hatch mcp configure' command."""
|
|
34
|
+
# Updated to match current CLI: server_name is positional, --host is required, --command/--url are mutually exclusive
|
|
35
|
+
test_args = ['hatch', 'mcp', 'configure', 'weather-server', '--host', 'claude-desktop', '--command', 'python', '--args', 'weather.py']
|
|
36
|
+
|
|
37
|
+
with patch('sys.argv', test_args):
|
|
38
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager'):
|
|
39
|
+
with patch('hatch.cli_hatch.handle_mcp_configure', return_value=0) as mock_handler:
|
|
40
|
+
try:
|
|
41
|
+
result = main()
|
|
42
|
+
# If main() returns without SystemExit, check the handler was called
|
|
43
|
+
# Updated to include ALL host-specific parameters
|
|
44
|
+
mock_handler.assert_called_once_with(
|
|
45
|
+
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
46
|
+
None, None, None, None, False, None, None, None, None, None, None, False, False, False
|
|
47
|
+
)
|
|
48
|
+
except SystemExit as e:
|
|
49
|
+
# If SystemExit is raised, it should be 0 (success) and handler should have been called
|
|
50
|
+
if e.code == 0:
|
|
51
|
+
mock_handler.assert_called_once_with(
|
|
52
|
+
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
53
|
+
None, None, None, None, False, None, None, None, None, None, None, False, False, False
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
self.fail(f"main() exited with code {e.code}, expected 0")
|
|
57
|
+
|
|
58
|
+
@regression_test
|
|
59
|
+
def test_configure_argument_parsing_with_options(self):
|
|
60
|
+
"""Test argument parsing with environment variables and options."""
|
|
61
|
+
test_args = [
|
|
62
|
+
'hatch', 'mcp', 'configure', 'file-server', '--host', 'cursor', '--url', 'http://localhost:8080',
|
|
63
|
+
'--env-var', 'API_KEY=secret', '--env-var', 'DEBUG=true',
|
|
64
|
+
'--header', 'Authorization=Bearer token',
|
|
65
|
+
'--no-backup', '--dry-run', '--auto-approve'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
with patch('sys.argv', test_args):
|
|
69
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager'):
|
|
70
|
+
with patch('hatch.cli_hatch.handle_mcp_configure', return_value=0) as mock_handler:
|
|
71
|
+
try:
|
|
72
|
+
main()
|
|
73
|
+
# Updated to include ALL host-specific parameters
|
|
74
|
+
mock_handler.assert_called_once_with(
|
|
75
|
+
'cursor', 'file-server', None, None,
|
|
76
|
+
['API_KEY=secret', 'DEBUG=true'], 'http://localhost:8080',
|
|
77
|
+
['Authorization=Bearer token'], None, False, None, None, None, None, None, None, True, True, True
|
|
78
|
+
)
|
|
79
|
+
except SystemExit as e:
|
|
80
|
+
self.assertEqual(e.code, 0)
|
|
81
|
+
|
|
82
|
+
@regression_test
|
|
83
|
+
def test_parse_env_vars(self):
|
|
84
|
+
"""Test environment variable parsing utility."""
|
|
85
|
+
# Valid environment variables
|
|
86
|
+
env_list = ['API_KEY=secret', 'DEBUG=true', 'PORT=8080']
|
|
87
|
+
result = parse_env_vars(env_list)
|
|
88
|
+
|
|
89
|
+
expected = {
|
|
90
|
+
'API_KEY': 'secret',
|
|
91
|
+
'DEBUG': 'true',
|
|
92
|
+
'PORT': '8080'
|
|
93
|
+
}
|
|
94
|
+
self.assertEqual(result, expected)
|
|
95
|
+
|
|
96
|
+
# Empty list
|
|
97
|
+
self.assertEqual(parse_env_vars(None), {})
|
|
98
|
+
self.assertEqual(parse_env_vars([]), {})
|
|
99
|
+
|
|
100
|
+
# Invalid format (should be skipped with warning)
|
|
101
|
+
with patch('builtins.print') as mock_print:
|
|
102
|
+
result = parse_env_vars(['INVALID_FORMAT', 'VALID=value'])
|
|
103
|
+
self.assertEqual(result, {'VALID': 'value'})
|
|
104
|
+
mock_print.assert_called()
|
|
105
|
+
|
|
106
|
+
@regression_test
|
|
107
|
+
def test_parse_header(self):
|
|
108
|
+
"""Test HTTP headers parsing utility."""
|
|
109
|
+
# Valid headers
|
|
110
|
+
headers_list = ['Authorization=Bearer token', 'Content-Type=application/json']
|
|
111
|
+
result = parse_header(headers_list)
|
|
112
|
+
|
|
113
|
+
expected = {
|
|
114
|
+
'Authorization': 'Bearer token',
|
|
115
|
+
'Content-Type': 'application/json'
|
|
116
|
+
}
|
|
117
|
+
self.assertEqual(result, expected)
|
|
118
|
+
|
|
119
|
+
# Empty list
|
|
120
|
+
self.assertEqual(parse_header(None), {})
|
|
121
|
+
self.assertEqual(parse_header([]), {})
|
|
122
|
+
|
|
123
|
+
@integration_test(scope="component")
|
|
124
|
+
def test_configure_invalid_host(self):
|
|
125
|
+
"""Test configure command with invalid host type."""
|
|
126
|
+
with patch('builtins.print') as mock_print:
|
|
127
|
+
result = handle_mcp_configure('invalid-host', 'test-server', 'python', ['test.py'])
|
|
128
|
+
|
|
129
|
+
self.assertEqual(result, 1)
|
|
130
|
+
|
|
131
|
+
# Verify error message
|
|
132
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
133
|
+
self.assertTrue(any("Error: Invalid host 'invalid-host'" in call for call in print_calls))
|
|
134
|
+
|
|
135
|
+
@integration_test(scope="component")
|
|
136
|
+
def test_configure_dry_run(self):
|
|
137
|
+
"""Test configure command dry run functionality."""
|
|
138
|
+
with patch('builtins.print') as mock_print:
|
|
139
|
+
result = handle_mcp_configure(
|
|
140
|
+
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
141
|
+
env=['API_KEY=secret'], url=None,
|
|
142
|
+
dry_run=True
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self.assertEqual(result, 0)
|
|
146
|
+
|
|
147
|
+
# Verify dry run output
|
|
148
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
149
|
+
self.assertTrue(any("[DRY RUN] Would configure MCP server 'weather-server'" in call for call in print_calls))
|
|
150
|
+
self.assertTrue(any("[DRY RUN] Command: python" in call for call in print_calls))
|
|
151
|
+
self.assertTrue(any("[DRY RUN] Environment:" in call for call in print_calls))
|
|
152
|
+
# URL should not be present for local server configuration
|
|
153
|
+
|
|
154
|
+
@integration_test(scope="component")
|
|
155
|
+
def test_configure_successful(self):
|
|
156
|
+
"""Test successful MCP server configuration."""
|
|
157
|
+
from hatch.mcp_host_config.host_management import ConfigurationResult
|
|
158
|
+
|
|
159
|
+
mock_result = ConfigurationResult(
|
|
160
|
+
success=True,
|
|
161
|
+
hostname='claude-desktop',
|
|
162
|
+
server_name='weather-server',
|
|
163
|
+
backup_path=Path('/test/backup.json')
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
167
|
+
mock_manager = MagicMock()
|
|
168
|
+
mock_manager.configure_server.return_value = mock_result
|
|
169
|
+
mock_manager_class.return_value = mock_manager
|
|
170
|
+
|
|
171
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
172
|
+
with patch('builtins.print') as mock_print:
|
|
173
|
+
result = handle_mcp_configure(
|
|
174
|
+
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
175
|
+
auto_approve=True
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
self.assertEqual(result, 0)
|
|
179
|
+
mock_manager.configure_server.assert_called_once()
|
|
180
|
+
|
|
181
|
+
# Verify success message
|
|
182
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
183
|
+
self.assertTrue(any("[SUCCESS] Successfully configured MCP server 'weather-server'" in call for call in print_calls))
|
|
184
|
+
self.assertTrue(any("Backup created:" in call for call in print_calls))
|
|
185
|
+
|
|
186
|
+
@integration_test(scope="component")
|
|
187
|
+
def test_configure_failed(self):
|
|
188
|
+
"""Test failed MCP server configuration."""
|
|
189
|
+
from hatch.mcp_host_config.host_management import ConfigurationResult
|
|
190
|
+
|
|
191
|
+
mock_result = ConfigurationResult(
|
|
192
|
+
success=False,
|
|
193
|
+
hostname='claude-desktop',
|
|
194
|
+
server_name='weather-server',
|
|
195
|
+
error_message='Configuration validation failed'
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
199
|
+
mock_manager = MagicMock()
|
|
200
|
+
mock_manager.configure_server.return_value = mock_result
|
|
201
|
+
mock_manager_class.return_value = mock_manager
|
|
202
|
+
|
|
203
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
204
|
+
with patch('builtins.print') as mock_print:
|
|
205
|
+
result = handle_mcp_configure(
|
|
206
|
+
'claude-desktop', 'weather-server', 'python', ['weather.py'],
|
|
207
|
+
auto_approve=True
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
self.assertEqual(result, 1)
|
|
211
|
+
|
|
212
|
+
# Verify error message
|
|
213
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
214
|
+
self.assertTrue(any("[ERROR] Failed to configure MCP server 'weather-server'" in call for call in print_calls))
|
|
215
|
+
self.assertTrue(any("Configuration validation failed" in call for call in print_calls))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class TestMCPRemoveCommand(unittest.TestCase):
|
|
219
|
+
"""Test suite for MCP remove command."""
|
|
220
|
+
|
|
221
|
+
@regression_test
|
|
222
|
+
def test_remove_argument_parsing(self):
|
|
223
|
+
"""Test argument parsing for 'hatch mcp remove server' command."""
|
|
224
|
+
test_args = ['hatch', 'mcp', 'remove', 'server', 'old-server', '--host', 'vscode', '--no-backup', '--auto-approve']
|
|
225
|
+
|
|
226
|
+
with patch('sys.argv', test_args):
|
|
227
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager'):
|
|
228
|
+
with patch('hatch.cli_hatch.handle_mcp_remove_server', return_value=0) as mock_handler:
|
|
229
|
+
try:
|
|
230
|
+
main()
|
|
231
|
+
mock_handler.assert_called_once_with(ANY, 'old-server', 'vscode', None, True, False, True)
|
|
232
|
+
except SystemExit as e:
|
|
233
|
+
self.assertEqual(e.code, 0)
|
|
234
|
+
|
|
235
|
+
@integration_test(scope="component")
|
|
236
|
+
def test_remove_invalid_host(self):
|
|
237
|
+
"""Test remove command with invalid host type."""
|
|
238
|
+
with patch('builtins.print') as mock_print:
|
|
239
|
+
result = handle_mcp_remove('invalid-host', 'test-server')
|
|
240
|
+
|
|
241
|
+
self.assertEqual(result, 1)
|
|
242
|
+
|
|
243
|
+
# Verify error message
|
|
244
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
245
|
+
self.assertTrue(any("Error: Invalid host 'invalid-host'" in call for call in print_calls))
|
|
246
|
+
|
|
247
|
+
@integration_test(scope="component")
|
|
248
|
+
def test_remove_dry_run(self):
|
|
249
|
+
"""Test remove command dry run functionality."""
|
|
250
|
+
with patch('builtins.print') as mock_print:
|
|
251
|
+
result = handle_mcp_remove('claude-desktop', 'old-server', no_backup=True, dry_run=True)
|
|
252
|
+
|
|
253
|
+
self.assertEqual(result, 0)
|
|
254
|
+
|
|
255
|
+
# Verify dry run output
|
|
256
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
257
|
+
self.assertTrue(any("[DRY RUN] Would remove MCP server 'old-server'" in call for call in print_calls))
|
|
258
|
+
self.assertTrue(any("[DRY RUN] Backup: Disabled" in call for call in print_calls))
|
|
259
|
+
|
|
260
|
+
@integration_test(scope="component")
|
|
261
|
+
def test_remove_successful(self):
|
|
262
|
+
"""Test successful MCP server removal."""
|
|
263
|
+
from hatch.mcp_host_config.host_management import ConfigurationResult
|
|
264
|
+
|
|
265
|
+
mock_result = ConfigurationResult(
|
|
266
|
+
success=True,
|
|
267
|
+
hostname='claude-desktop',
|
|
268
|
+
server_name='old-server',
|
|
269
|
+
backup_path=Path('/test/backup.json')
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
273
|
+
mock_manager = MagicMock()
|
|
274
|
+
mock_manager.remove_server.return_value = mock_result
|
|
275
|
+
mock_manager_class.return_value = mock_manager
|
|
276
|
+
|
|
277
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
278
|
+
with patch('builtins.print') as mock_print:
|
|
279
|
+
result = handle_mcp_remove('claude-desktop', 'old-server', auto_approve=True)
|
|
280
|
+
|
|
281
|
+
self.assertEqual(result, 0)
|
|
282
|
+
mock_manager.remove_server.assert_called_once()
|
|
283
|
+
|
|
284
|
+
# Verify success message
|
|
285
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
286
|
+
self.assertTrue(any("[SUCCESS] Successfully removed MCP server 'old-server'" in call for call in print_calls))
|
|
287
|
+
|
|
288
|
+
@integration_test(scope="component")
|
|
289
|
+
def test_remove_failed(self):
|
|
290
|
+
"""Test failed MCP server removal."""
|
|
291
|
+
from hatch.mcp_host_config.host_management import ConfigurationResult
|
|
292
|
+
|
|
293
|
+
mock_result = ConfigurationResult(
|
|
294
|
+
success=False,
|
|
295
|
+
hostname='claude-desktop',
|
|
296
|
+
server_name='old-server',
|
|
297
|
+
error_message='Server not found in configuration'
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
301
|
+
mock_manager = MagicMock()
|
|
302
|
+
mock_manager.remove_server.return_value = mock_result
|
|
303
|
+
mock_manager_class.return_value = mock_manager
|
|
304
|
+
|
|
305
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
306
|
+
with patch('builtins.print') as mock_print:
|
|
307
|
+
result = handle_mcp_remove('claude-desktop', 'old-server', auto_approve=True)
|
|
308
|
+
|
|
309
|
+
self.assertEqual(result, 1)
|
|
310
|
+
|
|
311
|
+
# Verify error message
|
|
312
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
313
|
+
self.assertTrue(any("[ERROR] Failed to remove MCP server 'old-server'" in call for call in print_calls))
|
|
314
|
+
self.assertTrue(any("Server not found in configuration" in call for call in print_calls))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class TestMCPRemoveServerCommand(unittest.TestCase):
|
|
318
|
+
"""Test suite for MCP remove server command (new object-action pattern)."""
|
|
319
|
+
|
|
320
|
+
@regression_test
|
|
321
|
+
def test_remove_server_argument_parsing(self):
|
|
322
|
+
"""Test argument parsing for 'hatch mcp remove server' command."""
|
|
323
|
+
test_args = ['hatch', 'mcp', 'remove', 'server', 'test-server', '--host', 'claude-desktop', '--no-backup']
|
|
324
|
+
|
|
325
|
+
with patch('sys.argv', test_args):
|
|
326
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager'):
|
|
327
|
+
with patch('hatch.cli_hatch.handle_mcp_remove_server', return_value=0) as mock_handler:
|
|
328
|
+
try:
|
|
329
|
+
main()
|
|
330
|
+
mock_handler.assert_called_once_with(ANY, 'test-server', 'claude-desktop', None, True, False, False)
|
|
331
|
+
except SystemExit as e:
|
|
332
|
+
self.assertEqual(e.code, 0)
|
|
333
|
+
|
|
334
|
+
@integration_test(scope="component")
|
|
335
|
+
def test_remove_server_multi_host(self):
|
|
336
|
+
"""Test remove server from multiple hosts."""
|
|
337
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
338
|
+
mock_manager = MagicMock()
|
|
339
|
+
mock_manager.remove_server.return_value = MagicMock(success=True, backup_path=None)
|
|
340
|
+
mock_manager_class.return_value = mock_manager
|
|
341
|
+
|
|
342
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
343
|
+
with patch('builtins.print') as mock_print:
|
|
344
|
+
result = handle_mcp_remove_server(mock_env_manager.return_value, 'test-server', 'claude-desktop,cursor', auto_approve=True)
|
|
345
|
+
|
|
346
|
+
self.assertEqual(result, 0)
|
|
347
|
+
self.assertEqual(mock_manager.remove_server.call_count, 2)
|
|
348
|
+
|
|
349
|
+
# Verify success messages
|
|
350
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
351
|
+
self.assertTrue(any("[SUCCESS] Successfully removed 'test-server' from 'claude-desktop'" in call for call in print_calls))
|
|
352
|
+
self.assertTrue(any("[SUCCESS] Successfully removed 'test-server' from 'cursor'" in call for call in print_calls))
|
|
353
|
+
|
|
354
|
+
@integration_test(scope="component")
|
|
355
|
+
def test_remove_server_no_host_specified(self):
|
|
356
|
+
"""Test remove server with no host specified."""
|
|
357
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
358
|
+
with patch('builtins.print') as mock_print:
|
|
359
|
+
result = handle_mcp_remove_server(mock_env_manager.return_value, 'test-server')
|
|
360
|
+
|
|
361
|
+
self.assertEqual(result, 1)
|
|
362
|
+
|
|
363
|
+
# Verify error message
|
|
364
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
365
|
+
self.assertTrue(any("Error: Must specify either --host or --env" in call for call in print_calls))
|
|
366
|
+
|
|
367
|
+
@integration_test(scope="component")
|
|
368
|
+
def test_remove_server_dry_run(self):
|
|
369
|
+
"""Test remove server dry run functionality."""
|
|
370
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
371
|
+
with patch('builtins.print') as mock_print:
|
|
372
|
+
result = handle_mcp_remove_server(mock_env_manager.return_value, 'test-server', 'claude-desktop', dry_run=True)
|
|
373
|
+
|
|
374
|
+
self.assertEqual(result, 0)
|
|
375
|
+
|
|
376
|
+
# Verify dry run output
|
|
377
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
378
|
+
self.assertTrue(any("[DRY RUN] Would remove MCP server 'test-server' from hosts: claude-desktop" in call for call in print_calls))
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TestMCPRemoveHostCommand(unittest.TestCase):
|
|
382
|
+
"""Test suite for MCP remove host command."""
|
|
383
|
+
|
|
384
|
+
@regression_test
|
|
385
|
+
def test_remove_host_argument_parsing(self):
|
|
386
|
+
"""Test argument parsing for 'hatch mcp remove host' command."""
|
|
387
|
+
test_args = ['hatch', 'mcp', 'remove', 'host', 'claude-desktop', '--auto-approve']
|
|
388
|
+
|
|
389
|
+
with patch('sys.argv', test_args):
|
|
390
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager'):
|
|
391
|
+
with patch('hatch.cli_hatch.handle_mcp_remove_host', return_value=0) as mock_handler:
|
|
392
|
+
try:
|
|
393
|
+
main()
|
|
394
|
+
mock_handler.assert_called_once_with(ANY, 'claude-desktop', False, False, True)
|
|
395
|
+
except SystemExit as e:
|
|
396
|
+
self.assertEqual(e.code, 0)
|
|
397
|
+
|
|
398
|
+
@integration_test(scope="component")
|
|
399
|
+
def test_remove_host_successful(self):
|
|
400
|
+
"""Test successful host configuration removal."""
|
|
401
|
+
with patch('hatch.cli_hatch.MCPHostConfigurationManager') as mock_manager_class:
|
|
402
|
+
mock_manager = MagicMock()
|
|
403
|
+
mock_result = MagicMock()
|
|
404
|
+
mock_result.success = True
|
|
405
|
+
mock_result.backup_path = Path("/test/backup.json")
|
|
406
|
+
mock_manager.remove_host_configuration.return_value = mock_result
|
|
407
|
+
mock_manager_class.return_value = mock_manager
|
|
408
|
+
|
|
409
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
410
|
+
# Mock the clear_host_from_all_packages_all_envs method
|
|
411
|
+
mock_env_manager.return_value.clear_host_from_all_packages_all_envs.return_value = 2
|
|
412
|
+
|
|
413
|
+
with patch('builtins.print') as mock_print:
|
|
414
|
+
result = handle_mcp_remove_host(mock_env_manager.return_value, 'claude-desktop', auto_approve=True)
|
|
415
|
+
|
|
416
|
+
self.assertEqual(result, 0)
|
|
417
|
+
mock_manager.remove_host_configuration.assert_called_once_with(
|
|
418
|
+
hostname='claude-desktop', no_backup=False
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Verify success message
|
|
422
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
423
|
+
self.assertTrue(any("[SUCCESS] Successfully removed host configuration for 'claude-desktop'" in call for call in print_calls))
|
|
424
|
+
|
|
425
|
+
@integration_test(scope="component")
|
|
426
|
+
def test_remove_host_invalid_host(self):
|
|
427
|
+
"""Test remove host with invalid host type."""
|
|
428
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
429
|
+
with patch('builtins.print') as mock_print:
|
|
430
|
+
result = handle_mcp_remove_host(mock_env_manager.return_value, 'invalid-host')
|
|
431
|
+
|
|
432
|
+
self.assertEqual(result, 1)
|
|
433
|
+
|
|
434
|
+
# Verify error message
|
|
435
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
436
|
+
self.assertTrue(any("Error: Invalid host 'invalid-host'" in call for call in print_calls))
|
|
437
|
+
|
|
438
|
+
@integration_test(scope="component")
|
|
439
|
+
def test_remove_host_dry_run(self):
|
|
440
|
+
"""Test remove host dry run functionality."""
|
|
441
|
+
with patch('hatch.cli_hatch.HatchEnvironmentManager') as mock_env_manager:
|
|
442
|
+
with patch('builtins.print') as mock_print:
|
|
443
|
+
result = handle_mcp_remove_host(mock_env_manager.return_value, 'claude-desktop', dry_run=True)
|
|
444
|
+
|
|
445
|
+
self.assertEqual(result, 0)
|
|
446
|
+
|
|
447
|
+
# Verify dry run output
|
|
448
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
449
|
+
self.assertTrue(any("[DRY RUN] Would remove entire host configuration for 'claude-desktop'" in call for call in print_calls))
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == '__main__':
|
|
453
|
+
unittest.main()
|