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,308 @@
1
+ """Tests for MCP backup system integration.
2
+
3
+ This module contains integration tests for the backup system with existing
4
+ Hatch infrastructure and end-to-end workflows.
5
+ """
6
+
7
+ import unittest
8
+ import tempfile
9
+ import shutil
10
+ import json
11
+ import time
12
+ from pathlib import Path
13
+ from unittest.mock import Mock, patch
14
+
15
+ from wobble.decorators import integration_test, slow_test, regression_test
16
+ from test_data_utils import MCPBackupTestDataLoader
17
+
18
+ from hatch.mcp_host_config.backup import (
19
+ MCPHostConfigBackupManager,
20
+ BackupAwareOperation,
21
+ BackupInfo,
22
+ BackupResult
23
+ )
24
+
25
+
26
+ class TestMCPBackupIntegration(unittest.TestCase):
27
+ """Test backup system integration with existing Hatch infrastructure."""
28
+
29
+ def setUp(self):
30
+ """Set up integration test environment."""
31
+ self.temp_dir = Path(tempfile.mkdtemp(prefix="test_integration_"))
32
+ self.backup_manager = MCPHostConfigBackupManager(backup_root=self.temp_dir / "backups")
33
+ self.test_data = MCPBackupTestDataLoader()
34
+
35
+ # Create test configuration files
36
+ self.config_dir = self.temp_dir / "configs"
37
+ self.config_dir.mkdir(parents=True)
38
+
39
+ self.test_configs = {}
40
+ for hostname in ['claude-desktop', 'claude-code', 'vscode', 'cursor']:
41
+ config_data = self.test_data.load_host_agnostic_config("simple_server")
42
+ config_file = self.config_dir / f"{hostname}_config.json"
43
+ with open(config_file, 'w') as f:
44
+ json.dump(config_data, f, indent=2)
45
+ self.test_configs[hostname] = config_file
46
+
47
+ def tearDown(self):
48
+ """Clean up integration test environment."""
49
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
50
+
51
+ @integration_test(scope="component")
52
+ def test_complete_backup_restore_cycle(self):
53
+ """Test complete backup creation and restoration cycle."""
54
+ hostname = 'claude-desktop'
55
+ config_file = self.test_configs[hostname]
56
+
57
+ # Create backup
58
+ backup_result = self.backup_manager.create_backup(config_file, hostname)
59
+ self.assertTrue(backup_result.success)
60
+
61
+ # Modify original file
62
+ modified_data = self.test_data.load_host_agnostic_config("complex_server")
63
+ with open(config_file, 'w') as f:
64
+ json.dump(modified_data, f)
65
+
66
+ # Verify file was modified
67
+ with open(config_file) as f:
68
+ current_data = json.load(f)
69
+ self.assertEqual(current_data, modified_data)
70
+
71
+ # Restore from backup (placeholder - actual restore would need host config paths)
72
+ restore_success = self.backup_manager.restore_backup(hostname)
73
+ self.assertTrue(restore_success) # Currently returns True as placeholder
74
+
75
+ @integration_test(scope="component")
76
+ def test_multi_host_backup_management(self):
77
+ """Test backup management across multiple hosts."""
78
+ # Create backups for multiple hosts
79
+ results = {}
80
+ for hostname, config_file in self.test_configs.items():
81
+ results[hostname] = self.backup_manager.create_backup(config_file, hostname)
82
+ self.assertTrue(results[hostname].success)
83
+
84
+ # Verify separate backup directories
85
+ for hostname in self.test_configs.keys():
86
+ backups = self.backup_manager.list_backups(hostname)
87
+ self.assertEqual(len(backups), 1)
88
+
89
+ # Verify backup isolation
90
+ backup_dir = backups[0].file_path.parent
91
+ self.assertEqual(backup_dir.name, hostname)
92
+
93
+ # Verify no cross-contamination
94
+ for other_hostname in self.test_configs.keys():
95
+ if other_hostname != hostname:
96
+ other_backups = self.backup_manager.list_backups(other_hostname)
97
+ self.assertNotEqual(
98
+ backups[0].file_path.parent,
99
+ other_backups[0].file_path.parent
100
+ )
101
+
102
+ @integration_test(scope="end_to_end")
103
+ def test_backup_with_configuration_update_workflow(self):
104
+ """Test backup integration with configuration update operations."""
105
+ hostname = 'vscode'
106
+ config_file = self.test_configs[hostname]
107
+
108
+ # Simulate configuration update with backup
109
+ original_data = self.test_data.load_host_agnostic_config("simple_server")
110
+ updated_data = self.test_data.load_host_agnostic_config("complex_server")
111
+
112
+ # Ensure original data is in file
113
+ with open(config_file, 'w') as f:
114
+ json.dump(original_data, f)
115
+
116
+ # Simulate update operation with backup
117
+ backup_result = self.backup_manager.create_backup(config_file, hostname)
118
+ self.assertTrue(backup_result.success)
119
+
120
+ # Update configuration
121
+ with open(config_file, 'w') as f:
122
+ json.dump(updated_data, f)
123
+
124
+ # Verify backup contains original data
125
+ backups = self.backup_manager.list_backups(hostname)
126
+ self.assertEqual(len(backups), 1)
127
+
128
+ with open(backups[0].file_path) as f:
129
+ backup_data = json.load(f)
130
+ self.assertEqual(backup_data, original_data)
131
+
132
+ # Verify current file has updated data
133
+ with open(config_file) as f:
134
+ current_data = json.load(f)
135
+ self.assertEqual(current_data, updated_data)
136
+
137
+ @integration_test(scope="service")
138
+ def test_backup_system_with_existing_test_utilities(self):
139
+ """Test backup system integration with existing test utilities."""
140
+ # Use existing TestDataLoader patterns
141
+ test_config = self.test_data.load_host_agnostic_config("complex_server")
142
+
143
+ # Test backup creation with complex configuration
144
+ config_path = self.temp_dir / "complex_config.json"
145
+ with open(config_path, 'w') as f:
146
+ json.dump(test_config, f)
147
+
148
+ result = self.backup_manager.create_backup(config_path, "lmstudio")
149
+ self.assertTrue(result.success)
150
+
151
+ # Verify integration with existing test data patterns
152
+ self.assertIsInstance(test_config, dict)
153
+ self.assertIn("servers", test_config)
154
+
155
+ # Verify backup content matches test data
156
+ with open(result.backup_path) as f:
157
+ backup_content = json.load(f)
158
+ self.assertEqual(backup_content, test_config)
159
+
160
+ @integration_test(scope="component")
161
+ def test_backup_aware_operation_workflow(self):
162
+ """Test backup-aware operation following environment manager patterns."""
163
+ hostname = 'cursor'
164
+ config_file = self.test_configs[hostname]
165
+
166
+ # Test backup-aware operation following existing patterns
167
+ operation = BackupAwareOperation(self.backup_manager)
168
+
169
+ # Simulate environment manager update workflow
170
+ backup_result = operation.prepare_backup(config_file, hostname, no_backup=False)
171
+ self.assertTrue(backup_result.success)
172
+
173
+ # Verify backup was created following existing patterns
174
+ backups = self.backup_manager.list_backups(hostname)
175
+ self.assertEqual(len(backups), 1)
176
+ self.assertEqual(backups[0].hostname, hostname)
177
+
178
+ # Test rollback capability
179
+ rollback_success = operation.rollback_on_failure(backup_result, config_file, hostname)
180
+ self.assertTrue(rollback_success)
181
+
182
+
183
+ class TestMCPBackupPerformance(unittest.TestCase):
184
+ """Test backup system performance characteristics."""
185
+
186
+ def setUp(self):
187
+ """Set up performance test environment."""
188
+ self.temp_dir = Path(tempfile.mkdtemp(prefix="test_performance_"))
189
+ self.backup_manager = MCPHostConfigBackupManager(backup_root=self.temp_dir / "backups")
190
+ self.test_data = MCPBackupTestDataLoader()
191
+
192
+ def tearDown(self):
193
+ """Clean up performance test environment."""
194
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
195
+
196
+ @slow_test
197
+ @regression_test
198
+ def test_backup_performance_large_config(self):
199
+ """Test backup performance with larger configuration files."""
200
+ # Create large host-agnostic configuration
201
+ large_config = {"servers": {}}
202
+ for i in range(1000):
203
+ large_config["servers"][f"server_{i}"] = {
204
+ "command": f"python_{i}",
205
+ "args": [f"arg_{j}" for j in range(10)]
206
+ }
207
+
208
+ config_file = self.temp_dir / "large_config.json"
209
+ with open(config_file, 'w') as f:
210
+ json.dump(large_config, f)
211
+
212
+ start_time = time.time()
213
+ result = self.backup_manager.create_backup(config_file, "gemini")
214
+ duration = time.time() - start_time
215
+
216
+ self.assertTrue(result.success)
217
+ self.assertLess(duration, 1.0) # Should complete within 1 second
218
+
219
+ @regression_test
220
+ def test_pydantic_validation_performance(self):
221
+ """Test Pydantic model validation performance."""
222
+ hostname = "claude-desktop"
223
+ config_data = self.test_data.load_host_agnostic_config("simple_server")
224
+ config_file = self.temp_dir / "test_config.json"
225
+
226
+ with open(config_file, 'w') as f:
227
+ json.dump(config_data, f)
228
+
229
+ start_time = time.time()
230
+
231
+ # Create backup (includes Pydantic validation)
232
+ result = self.backup_manager.create_backup(config_file, hostname)
233
+
234
+ # List backups (includes Pydantic model creation)
235
+ backups = self.backup_manager.list_backups(hostname)
236
+
237
+ duration = time.time() - start_time
238
+
239
+ self.assertTrue(result.success)
240
+ self.assertEqual(len(backups), 1)
241
+ self.assertLess(duration, 0.1) # Pydantic operations should be fast
242
+
243
+ @regression_test
244
+ def test_concurrent_backup_operations(self):
245
+ """Test concurrent backup operations for different hosts."""
246
+ import threading
247
+
248
+ results = {}
249
+ config_files = {}
250
+
251
+ # Create test configurations for different hosts
252
+ for hostname in ['claude-desktop', 'vscode', 'cursor', 'lmstudio']:
253
+ config_data = self.test_data.load_host_agnostic_config("simple_server")
254
+ config_file = self.temp_dir / f"{hostname}_config.json"
255
+ with open(config_file, 'w') as f:
256
+ json.dump(config_data, f)
257
+ config_files[hostname] = config_file
258
+
259
+ def create_backup_thread(hostname, config_file):
260
+ results[hostname] = self.backup_manager.create_backup(config_file, hostname)
261
+
262
+ # Start concurrent backup operations
263
+ threads = []
264
+ for hostname, config_file in config_files.items():
265
+ thread = threading.Thread(target=create_backup_thread, args=(hostname, config_file))
266
+ threads.append(thread)
267
+ thread.start()
268
+
269
+ # Wait for all threads to complete
270
+ for thread in threads:
271
+ thread.join(timeout=5.0)
272
+
273
+ # Verify all operations succeeded
274
+ for hostname in config_files.keys():
275
+ self.assertIn(hostname, results)
276
+ self.assertTrue(results[hostname].success)
277
+
278
+ @regression_test
279
+ def test_backup_list_performance_many_backups(self):
280
+ """Test backup listing performance with many backup files."""
281
+ hostname = "claude-code"
282
+ config_data = self.test_data.load_host_agnostic_config("simple_server")
283
+ config_file = self.temp_dir / "test_config.json"
284
+
285
+ with open(config_file, 'w') as f:
286
+ json.dump(config_data, f)
287
+
288
+ # Create many backups
289
+ for i in range(50):
290
+ result = self.backup_manager.create_backup(config_file, hostname)
291
+ self.assertTrue(result.success)
292
+
293
+ # Test listing performance
294
+ start_time = time.time()
295
+ backups = self.backup_manager.list_backups(hostname)
296
+ duration = time.time() - start_time
297
+
298
+ self.assertEqual(len(backups), 50)
299
+ self.assertLess(duration, 0.1) # Should be fast even with many backups
300
+
301
+ # Verify all backups are valid Pydantic models
302
+ for backup in backups:
303
+ self.assertIsInstance(backup, BackupInfo)
304
+ self.assertEqual(backup.hostname, hostname)
305
+
306
+
307
+ if __name__ == '__main__':
308
+ unittest.main()
@@ -0,0 +1,303 @@
1
+ """
2
+ Tests for ALL host-specific CLI arguments in MCP configure command.
3
+
4
+ This module tests that:
5
+ 1. All host-specific arguments are accepted for all hosts
6
+ 2. Unsupported fields are reported as "UNSUPPORTED" in conversion reports
7
+ 3. All new arguments (httpUrl, includeTools, excludeTools, inputs) work correctly
8
+ """
9
+
10
+ import unittest
11
+ from unittest.mock import patch, MagicMock
12
+ from io import StringIO
13
+
14
+ from hatch.cli_hatch import handle_mcp_configure, parse_input
15
+ from hatch.mcp_host_config import MCPHostType
16
+ from hatch.mcp_host_config.models import (
17
+ MCPServerConfigGemini, MCPServerConfigCursor, MCPServerConfigVSCode,
18
+ MCPServerConfigClaude
19
+ )
20
+
21
+
22
+ class TestAllGeminiArguments(unittest.TestCase):
23
+ """Test ALL Gemini-specific CLI arguments."""
24
+
25
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
26
+ @patch('sys.stdout', new_callable=StringIO)
27
+ def test_all_gemini_arguments_accepted(self, mock_stdout, mock_manager_class):
28
+ """Test that all Gemini arguments are accepted and passed to model."""
29
+ mock_manager = MagicMock()
30
+ mock_manager_class.return_value = mock_manager
31
+
32
+ mock_result = MagicMock()
33
+ mock_result.success = True
34
+ mock_result.backup_path = None
35
+ mock_manager.configure_server.return_value = mock_result
36
+
37
+ result = handle_mcp_configure(
38
+ host='gemini',
39
+ server_name='test-server',
40
+ command='python',
41
+ args=['server.py'],
42
+ timeout=30000,
43
+ trust=True,
44
+ cwd='/workspace',
45
+ http_url='https://api.example.com/mcp',
46
+ include_tools=['tool1', 'tool2'],
47
+ exclude_tools=['dangerous_tool'],
48
+ auto_approve=True
49
+ )
50
+
51
+ self.assertEqual(result, 0)
52
+
53
+ # Verify all fields were passed to Gemini model
54
+ call_args = mock_manager.configure_server.call_args
55
+ server_config = call_args.kwargs['server_config']
56
+ self.assertIsInstance(server_config, MCPServerConfigGemini)
57
+ self.assertEqual(server_config.timeout, 30000)
58
+ self.assertEqual(server_config.trust, True)
59
+ self.assertEqual(server_config.cwd, '/workspace')
60
+ self.assertEqual(server_config.httpUrl, 'https://api.example.com/mcp')
61
+ self.assertEqual(server_config.includeTools, ['tool1', 'tool2'])
62
+ self.assertEqual(server_config.excludeTools, ['dangerous_tool'])
63
+
64
+
65
+ class TestUnsupportedFieldReporting(unittest.TestCase):
66
+ """Test that unsupported fields are reported correctly, not rejected."""
67
+
68
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
69
+ @patch('sys.stdout', new_callable=StringIO)
70
+ def test_gemini_args_on_vscode_show_unsupported(self, mock_stdout, mock_manager_class):
71
+ """Test that Gemini-specific args on VS Code show as UNSUPPORTED."""
72
+ mock_manager = MagicMock()
73
+ mock_manager_class.return_value = mock_manager
74
+
75
+ mock_result = MagicMock()
76
+ mock_result.success = True
77
+ mock_result.backup_path = None
78
+ mock_manager.configure_server.return_value = mock_result
79
+
80
+ result = handle_mcp_configure(
81
+ host='vscode',
82
+ server_name='test-server',
83
+ command='python',
84
+ args=['server.py'],
85
+ timeout=30000, # Gemini-only field
86
+ trust=True, # Gemini-only field
87
+ auto_approve=True
88
+ )
89
+
90
+ # Should succeed (not return error code 1)
91
+ self.assertEqual(result, 0)
92
+
93
+ # Check that output contains "UNSUPPORTED" for Gemini fields
94
+ output = mock_stdout.getvalue()
95
+ self.assertIn('UNSUPPORTED', output)
96
+ self.assertIn('timeout', output)
97
+ self.assertIn('trust', output)
98
+
99
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
100
+ @patch('sys.stdout', new_callable=StringIO)
101
+ def test_vscode_inputs_on_gemini_show_unsupported(self, mock_stdout, mock_manager_class):
102
+ """Test that VS Code inputs on Gemini show as UNSUPPORTED."""
103
+ mock_manager = MagicMock()
104
+ mock_manager_class.return_value = mock_manager
105
+
106
+ mock_result = MagicMock()
107
+ mock_result.success = True
108
+ mock_result.backup_path = None
109
+ mock_manager.configure_server.return_value = mock_result
110
+
111
+ result = handle_mcp_configure(
112
+ host='gemini',
113
+ server_name='test-server',
114
+ command='python',
115
+ args=['server.py'],
116
+ input=['promptString,api-key,API Key,password=true'], # VS Code-only field
117
+ auto_approve=True
118
+ )
119
+
120
+ # Should succeed (not return error code 1)
121
+ self.assertEqual(result, 0)
122
+
123
+ # Check that output contains "UNSUPPORTED" for inputs field
124
+ output = mock_stdout.getvalue()
125
+ self.assertIn('UNSUPPORTED', output)
126
+ self.assertIn('inputs', output)
127
+
128
+
129
+ class TestVSCodeInputsParsing(unittest.TestCase):
130
+ """Test VS Code inputs parsing."""
131
+
132
+ def test_parse_input_basic(self):
133
+ """Test basic input parsing."""
134
+ input_list = ['promptString,api-key,GitHub Personal Access Token']
135
+ result = parse_input(input_list)
136
+
137
+ self.assertIsNotNone(result)
138
+ self.assertEqual(len(result), 1)
139
+ self.assertEqual(result[0]['type'], 'promptString')
140
+ self.assertEqual(result[0]['id'], 'api-key')
141
+ self.assertEqual(result[0]['description'], 'GitHub Personal Access Token')
142
+ self.assertNotIn('password', result[0])
143
+
144
+ def test_parse_input_with_password(self):
145
+ """Test input parsing with password flag."""
146
+ input_list = ['promptString,api-key,API Key,password=true']
147
+ result = parse_input(input_list)
148
+
149
+ self.assertIsNotNone(result)
150
+ self.assertEqual(len(result), 1)
151
+ self.assertEqual(result[0]['password'], True)
152
+
153
+ def test_parse_input_multiple(self):
154
+ """Test parsing multiple inputs."""
155
+ input_list = [
156
+ 'promptString,api-key,API Key,password=true',
157
+ 'promptString,db-url,Database URL'
158
+ ]
159
+ result = parse_input(input_list)
160
+
161
+ self.assertIsNotNone(result)
162
+ self.assertEqual(len(result), 2)
163
+
164
+ def test_parse_input_none(self):
165
+ """Test parsing None inputs."""
166
+ result = parse_input(None)
167
+ self.assertIsNone(result)
168
+
169
+ def test_parse_input_empty(self):
170
+ """Test parsing empty inputs list."""
171
+ result = parse_input([])
172
+ self.assertIsNone(result)
173
+
174
+
175
+ class TestVSCodeInputsIntegration(unittest.TestCase):
176
+ """Test VS Code inputs integration with configure command."""
177
+
178
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
179
+ def test_vscode_inputs_passed_to_model(self, mock_manager_class):
180
+ """Test that parsed inputs are passed to VS Code model."""
181
+ mock_manager = MagicMock()
182
+ mock_manager_class.return_value = mock_manager
183
+
184
+ mock_result = MagicMock()
185
+ mock_result.success = True
186
+ mock_result.backup_path = None
187
+ mock_manager.configure_server.return_value = mock_result
188
+
189
+ result = handle_mcp_configure(
190
+ host='vscode',
191
+ server_name='test-server',
192
+ command='python',
193
+ args=['server.py'],
194
+ input=['promptString,api-key,API Key,password=true'],
195
+ auto_approve=True
196
+ )
197
+
198
+ self.assertEqual(result, 0)
199
+
200
+ # Verify inputs were passed to VS Code model
201
+ call_args = mock_manager.configure_server.call_args
202
+ server_config = call_args.kwargs['server_config']
203
+ self.assertIsInstance(server_config, MCPServerConfigVSCode)
204
+ self.assertIsNotNone(server_config.inputs)
205
+ self.assertEqual(len(server_config.inputs), 1)
206
+ self.assertEqual(server_config.inputs[0]['id'], 'api-key')
207
+
208
+
209
+ class TestHttpUrlArgument(unittest.TestCase):
210
+ """Test --http-url argument for Gemini."""
211
+
212
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
213
+ def test_http_url_passed_to_gemini(self, mock_manager_class):
214
+ """Test that httpUrl is passed to Gemini model."""
215
+ mock_manager = MagicMock()
216
+ mock_manager_class.return_value = mock_manager
217
+
218
+ mock_result = MagicMock()
219
+ mock_result.success = True
220
+ mock_result.backup_path = None
221
+ mock_manager.configure_server.return_value = mock_result
222
+
223
+ result = handle_mcp_configure(
224
+ host='gemini',
225
+ server_name='test-server',
226
+ command='python',
227
+ args=['server.py'],
228
+ http_url='https://api.example.com/mcp',
229
+ auto_approve=True
230
+ )
231
+
232
+ self.assertEqual(result, 0)
233
+
234
+ # Verify httpUrl was passed to Gemini model
235
+ call_args = mock_manager.configure_server.call_args
236
+ server_config = call_args.kwargs['server_config']
237
+ self.assertIsInstance(server_config, MCPServerConfigGemini)
238
+ self.assertEqual(server_config.httpUrl, 'https://api.example.com/mcp')
239
+
240
+
241
+ class TestToolFilteringArguments(unittest.TestCase):
242
+ """Test --include-tools and --exclude-tools arguments for Gemini."""
243
+
244
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
245
+ def test_include_tools_passed_to_gemini(self, mock_manager_class):
246
+ """Test that includeTools is passed to Gemini model."""
247
+ mock_manager = MagicMock()
248
+ mock_manager_class.return_value = mock_manager
249
+
250
+ mock_result = MagicMock()
251
+ mock_result.success = True
252
+ mock_result.backup_path = None
253
+ mock_manager.configure_server.return_value = mock_result
254
+
255
+ result = handle_mcp_configure(
256
+ host='gemini',
257
+ server_name='test-server',
258
+ command='python',
259
+ args=['server.py'],
260
+ include_tools=['tool1', 'tool2', 'tool3'],
261
+ auto_approve=True
262
+ )
263
+
264
+ self.assertEqual(result, 0)
265
+
266
+ # Verify includeTools was passed to Gemini model
267
+ call_args = mock_manager.configure_server.call_args
268
+ server_config = call_args.kwargs['server_config']
269
+ self.assertIsInstance(server_config, MCPServerConfigGemini)
270
+ self.assertEqual(server_config.includeTools, ['tool1', 'tool2', 'tool3'])
271
+
272
+ @patch('hatch.cli_hatch.MCPHostConfigurationManager')
273
+ def test_exclude_tools_passed_to_gemini(self, mock_manager_class):
274
+ """Test that excludeTools is passed to Gemini model."""
275
+ mock_manager = MagicMock()
276
+ mock_manager_class.return_value = mock_manager
277
+
278
+ mock_result = MagicMock()
279
+ mock_result.success = True
280
+ mock_result.backup_path = None
281
+ mock_manager.configure_server.return_value = mock_result
282
+
283
+ result = handle_mcp_configure(
284
+ host='gemini',
285
+ server_name='test-server',
286
+ command='python',
287
+ args=['server.py'],
288
+ exclude_tools=['dangerous_tool'],
289
+ auto_approve=True
290
+ )
291
+
292
+ self.assertEqual(result, 0)
293
+
294
+ # Verify excludeTools was passed to Gemini model
295
+ call_args = mock_manager.configure_server.call_args
296
+ server_config = call_args.kwargs['server_config']
297
+ self.assertIsInstance(server_config, MCPServerConfigGemini)
298
+ self.assertEqual(server_config.excludeTools, ['dangerous_tool'])
299
+
300
+
301
+ if __name__ == '__main__':
302
+ unittest.main()
303
+