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