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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. 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()