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,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
+