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,257 @@
1
+ """Tests for MCPHostConfigBackupManager.
2
+
3
+ This module contains tests for the MCP host configuration backup functionality,
4
+ including backup creation, restoration, and management with host-agnostic design.
5
+ """
6
+
7
+ import unittest
8
+ import tempfile
9
+ import shutil
10
+ import json
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+ from unittest.mock import patch, Mock
14
+
15
+ from wobble.decorators import regression_test, integration_test, slow_test
16
+ from test_data_utils import MCPBackupTestDataLoader
17
+
18
+ from hatch.mcp_host_config.backup import (
19
+ MCPHostConfigBackupManager,
20
+ BackupInfo,
21
+ BackupResult,
22
+ BackupError
23
+ )
24
+
25
+
26
+ class TestMCPHostConfigBackupManager(unittest.TestCase):
27
+ """Test MCPHostConfigBackupManager core functionality with host-agnostic design."""
28
+
29
+ def setUp(self):
30
+ """Set up test environment with host-agnostic configurations."""
31
+ self.temp_dir = Path(tempfile.mkdtemp(prefix="test_mcp_backup_"))
32
+ self.backup_root = self.temp_dir / "backups"
33
+ self.config_dir = self.temp_dir / "configs"
34
+ self.config_dir.mkdir(parents=True)
35
+
36
+ # Initialize test data loader
37
+ self.test_data = MCPBackupTestDataLoader()
38
+
39
+ # Create host-agnostic test configuration files
40
+ self.test_configs = {}
41
+ for hostname in ['claude-desktop', 'vscode', 'cursor', 'lmstudio']:
42
+ config_data = self.test_data.load_host_agnostic_config("simple_server")
43
+ config_file = self.config_dir / f"{hostname}_config.json"
44
+ with open(config_file, 'w') as f:
45
+ json.dump(config_data, f, indent=2)
46
+ self.test_configs[hostname] = config_file
47
+
48
+ self.backup_manager = MCPHostConfigBackupManager(backup_root=self.backup_root)
49
+
50
+ def tearDown(self):
51
+ """Clean up test environment."""
52
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
53
+
54
+ @regression_test
55
+ def test_backup_directory_creation(self):
56
+ """Test automatic backup directory creation."""
57
+ self.assertTrue(self.backup_root.exists())
58
+ self.assertTrue(self.backup_root.is_dir())
59
+
60
+ @regression_test
61
+ def test_create_backup_success_all_hosts(self):
62
+ """Test successful backup creation for all supported host types."""
63
+ for hostname, config_file in self.test_configs.items():
64
+ with self.subTest(hostname=hostname):
65
+ result = self.backup_manager.create_backup(config_file, hostname)
66
+
67
+ # Validate BackupResult Pydantic model
68
+ self.assertIsInstance(result, BackupResult)
69
+ self.assertTrue(result.success)
70
+ self.assertIsNotNone(result.backup_path)
71
+ self.assertTrue(result.backup_path.exists())
72
+ self.assertGreater(result.backup_size, 0)
73
+ self.assertEqual(result.original_size, result.backup_size)
74
+
75
+ # Verify backup filename format (host-agnostic)
76
+ expected_pattern = rf"mcp\.json\.{hostname}\.\d{{8}}_\d{{6}}_\d{{6}}"
77
+ self.assertRegex(result.backup_path.name, expected_pattern)
78
+
79
+ @regression_test
80
+ def test_create_backup_nonexistent_file(self):
81
+ """Test backup creation with nonexistent source file."""
82
+ nonexistent = self.config_dir / "nonexistent.json"
83
+ result = self.backup_manager.create_backup(nonexistent, "claude-desktop")
84
+
85
+ self.assertFalse(result.success)
86
+ self.assertIsNotNone(result.error_message)
87
+ self.assertIn("not found", result.error_message.lower())
88
+
89
+ @regression_test
90
+ def test_backup_content_integrity_host_agnostic(self):
91
+ """Test backup content matches original for any host configuration format."""
92
+ hostname = 'claude-desktop'
93
+ config_file = self.test_configs[hostname]
94
+ original_content = config_file.read_text()
95
+
96
+ result = self.backup_manager.create_backup(config_file, hostname)
97
+
98
+ self.assertTrue(result.success)
99
+ backup_content = result.backup_path.read_text()
100
+ self.assertEqual(original_content, backup_content)
101
+
102
+ # Verify JSON structure is preserved (host-agnostic validation)
103
+ original_json = json.loads(original_content)
104
+ backup_json = json.loads(backup_content)
105
+ self.assertEqual(original_json, backup_json)
106
+
107
+ @regression_test
108
+ def test_multiple_backups_same_host(self):
109
+ """Test creating multiple backups for same host."""
110
+ hostname = 'vscode'
111
+ config_file = self.test_configs[hostname]
112
+
113
+ # Create first backup
114
+ result1 = self.backup_manager.create_backup(config_file, hostname)
115
+ self.assertTrue(result1.success)
116
+
117
+ # Modify config and create second backup
118
+ modified_config = self.test_data.load_host_agnostic_config("complex_server")
119
+ with open(config_file, 'w') as f:
120
+ json.dump(modified_config, f, indent=2)
121
+
122
+ result2 = self.backup_manager.create_backup(config_file, hostname)
123
+ self.assertTrue(result2.success)
124
+
125
+ # Verify both backups exist and are different
126
+ self.assertTrue(result1.backup_path.exists())
127
+ self.assertTrue(result2.backup_path.exists())
128
+ self.assertNotEqual(result1.backup_path, result2.backup_path)
129
+
130
+ @regression_test
131
+ def test_list_backups_empty(self):
132
+ """Test listing backups when none exist."""
133
+ backups = self.backup_manager.list_backups("claude-desktop")
134
+ self.assertEqual(len(backups), 0)
135
+
136
+ @regression_test
137
+ def test_list_backups_pydantic_validation(self):
138
+ """Test listing backups returns valid Pydantic models."""
139
+ hostname = 'cursor'
140
+ config_file = self.test_configs[hostname]
141
+
142
+ # Create multiple backups
143
+ self.backup_manager.create_backup(config_file, hostname)
144
+ self.backup_manager.create_backup(config_file, hostname)
145
+
146
+ backups = self.backup_manager.list_backups(hostname)
147
+ self.assertEqual(len(backups), 2)
148
+
149
+ # Verify BackupInfo Pydantic model validation
150
+ for backup in backups:
151
+ self.assertIsInstance(backup, BackupInfo)
152
+ self.assertEqual(backup.hostname, hostname)
153
+ self.assertIsInstance(backup.timestamp, datetime)
154
+ self.assertTrue(backup.file_path.exists())
155
+ self.assertGreater(backup.file_size, 0)
156
+
157
+ # Test Pydantic serialization
158
+ backup_dict = backup.dict()
159
+ self.assertIn('hostname', backup_dict)
160
+ self.assertIn('timestamp', backup_dict)
161
+
162
+ # Test JSON serialization
163
+ backup_json = backup.json()
164
+ self.assertIsInstance(backup_json, str)
165
+
166
+ # Verify sorting (newest first)
167
+ self.assertGreaterEqual(backups[0].timestamp, backups[1].timestamp)
168
+
169
+ @regression_test
170
+ def test_backup_validation_unsupported_hostname(self):
171
+ """Test Pydantic validation rejects unsupported hostnames."""
172
+ config_file = self.test_configs['claude-desktop']
173
+
174
+ # Test with unsupported hostname
175
+ result = self.backup_manager.create_backup(config_file, 'unsupported-host')
176
+
177
+ self.assertFalse(result.success)
178
+ self.assertIn('unsupported', result.error_message.lower())
179
+
180
+ @regression_test
181
+ def test_multiple_hosts_isolation(self):
182
+ """Test backup isolation between different host types."""
183
+ # Create backups for multiple hosts
184
+ results = {}
185
+ for hostname, config_file in self.test_configs.items():
186
+ results[hostname] = self.backup_manager.create_backup(config_file, hostname)
187
+ self.assertTrue(results[hostname].success)
188
+
189
+ # Verify separate backup directories
190
+ for hostname in self.test_configs.keys():
191
+ backups = self.backup_manager.list_backups(hostname)
192
+ self.assertEqual(len(backups), 1)
193
+
194
+ # Verify backup isolation (different directories)
195
+ backup_dir = backups[0].file_path.parent
196
+ self.assertEqual(backup_dir.name, hostname)
197
+
198
+ # Verify no cross-contamination
199
+ for other_hostname in self.test_configs.keys():
200
+ if other_hostname != hostname:
201
+ other_backups = self.backup_manager.list_backups(other_hostname)
202
+ self.assertNotEqual(
203
+ backups[0].file_path.parent,
204
+ other_backups[0].file_path.parent
205
+ )
206
+
207
+ @regression_test
208
+ def test_clean_backups_older_than_days(self):
209
+ """Test cleaning backups older than specified days."""
210
+ hostname = 'lmstudio'
211
+ config_file = self.test_configs[hostname]
212
+
213
+ # Create backup
214
+ result = self.backup_manager.create_backup(config_file, hostname)
215
+ self.assertTrue(result.success)
216
+
217
+ # Mock old backup by modifying timestamp
218
+ old_backup_path = result.backup_path.parent / "mcp.json.lmstudio.20200101_120000_000000"
219
+ shutil.copy2(result.backup_path, old_backup_path)
220
+
221
+ # Clean backups older than 1 day (should remove the old one)
222
+ cleaned_count = self.backup_manager.clean_backups(hostname, older_than_days=1)
223
+
224
+ # Verify old backup was cleaned
225
+ self.assertGreater(cleaned_count, 0)
226
+ self.assertFalse(old_backup_path.exists())
227
+ self.assertTrue(result.backup_path.exists()) # Recent backup should remain
228
+
229
+ @regression_test
230
+ def test_clean_backups_keep_count(self):
231
+ """Test cleaning backups to keep only specified count."""
232
+ hostname = 'claude-desktop'
233
+ config_file = self.test_configs[hostname]
234
+
235
+ # Create multiple backups
236
+ for i in range(5):
237
+ self.backup_manager.create_backup(config_file, hostname)
238
+
239
+ # Verify 5 backups exist
240
+ backups_before = self.backup_manager.list_backups(hostname)
241
+ self.assertEqual(len(backups_before), 5)
242
+
243
+ # Clean to keep only 2 backups
244
+ cleaned_count = self.backup_manager.clean_backups(hostname, keep_count=2)
245
+
246
+ # Verify only 2 backups remain
247
+ backups_after = self.backup_manager.list_backups(hostname)
248
+ self.assertEqual(len(backups_after), 2)
249
+ self.assertEqual(cleaned_count, 3)
250
+
251
+ # Verify newest backups were kept
252
+ for backup in backups_after:
253
+ self.assertIn(backup, backups_before[:2]) # Should be the first 2 (newest)
254
+
255
+
256
+ if __name__ == '__main__':
257
+ unittest.main()
@@ -0,0 +1,331 @@
1
+ """
2
+ Test suite for MCP host configuration manager.
3
+
4
+ This module tests the core configuration manager with consolidated models
5
+ and integration with backup system.
6
+ """
7
+
8
+ import unittest
9
+ import sys
10
+ from pathlib import Path
11
+ import tempfile
12
+ import json
13
+ import os
14
+
15
+ # Add the parent directory to the path to import wobble
16
+ sys.path.insert(0, str(Path(__file__).parent.parent))
17
+
18
+ try:
19
+ from wobble.decorators import regression_test, integration_test
20
+ except ImportError:
21
+ # Fallback decorators if wobble is not available
22
+ def regression_test(func):
23
+ return func
24
+
25
+ def integration_test(scope="component"):
26
+ def decorator(func):
27
+ return func
28
+ return decorator
29
+
30
+ from test_data_utils import MCPHostConfigTestDataLoader
31
+ from hatch.mcp_host_config.host_management import MCPHostConfigurationManager, MCPHostRegistry, register_host_strategy
32
+ from hatch.mcp_host_config.models import MCPHostType, MCPServerConfig, HostConfiguration, ConfigurationResult, SyncResult
33
+ from hatch.mcp_host_config.strategies import MCPHostStrategy
34
+
35
+
36
+ class TestMCPHostConfigurationManager(unittest.TestCase):
37
+ """Test suite for MCP host configuration manager."""
38
+
39
+ def setUp(self):
40
+ """Set up test environment."""
41
+ self.test_data_loader = MCPHostConfigTestDataLoader()
42
+ self.temp_dir = tempfile.mkdtemp()
43
+ self.temp_config_path = Path(self.temp_dir) / "test_config.json"
44
+
45
+ # Clear registry before each test
46
+ MCPHostRegistry._strategies.clear()
47
+ MCPHostRegistry._instances.clear()
48
+
49
+ # Store temp_config_path for strategy access
50
+ temp_config_path = self.temp_config_path
51
+
52
+ # Register test strategy
53
+ @register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
54
+ class TestStrategy(MCPHostStrategy):
55
+ def get_config_path(self):
56
+ return temp_config_path
57
+
58
+ def is_host_available(self):
59
+ return True
60
+
61
+ def read_configuration(self):
62
+ if temp_config_path.exists():
63
+ with open(temp_config_path, 'r') as f:
64
+ data = json.load(f)
65
+
66
+ servers = {}
67
+ if "mcpServers" in data:
68
+ for name, config in data["mcpServers"].items():
69
+ servers[name] = MCPServerConfig(**config)
70
+
71
+ return HostConfiguration(servers=servers)
72
+ else:
73
+ return HostConfiguration(servers={})
74
+
75
+ def write_configuration(self, config, no_backup=False):
76
+ try:
77
+ # Convert MCPServerConfig objects to dict
78
+ servers_dict = {}
79
+ for name, server_config in config.servers.items():
80
+ servers_dict[name] = server_config.model_dump(exclude_none=True)
81
+
82
+ # Create configuration data
83
+ config_data = {"mcpServers": servers_dict}
84
+
85
+ # Write to file
86
+ with open(temp_config_path, 'w') as f:
87
+ json.dump(config_data, f, indent=2)
88
+
89
+ return True
90
+ except Exception:
91
+ return False
92
+
93
+ def validate_server_config(self, server_config):
94
+ return True
95
+
96
+ self.manager = MCPHostConfigurationManager()
97
+ self.temp_config_path = self.temp_config_path
98
+
99
+ def tearDown(self):
100
+ """Clean up test environment."""
101
+ # Clean up temp files
102
+ if self.temp_config_path.exists():
103
+ self.temp_config_path.unlink()
104
+ os.rmdir(self.temp_dir)
105
+
106
+ # Clear registry after each test
107
+ MCPHostRegistry._strategies.clear()
108
+ MCPHostRegistry._instances.clear()
109
+
110
+ @regression_test
111
+ def test_configure_server_success(self):
112
+ """Test successful server configuration."""
113
+ server_config_data = self.test_data_loader.load_mcp_server_config("local")
114
+ server_config = MCPServerConfig(**server_config_data)
115
+ # Add name attribute for the manager to use
116
+ server_config.name = "test_server"
117
+
118
+ result = self.manager.configure_server(
119
+ server_config=server_config,
120
+ hostname="claude-desktop"
121
+ )
122
+
123
+ self.assertIsInstance(result, ConfigurationResult)
124
+ if not result.success:
125
+ print(f"Configuration failed: {result.error_message}")
126
+ self.assertTrue(result.success)
127
+ self.assertIsNone(result.error_message)
128
+ self.assertEqual(result.hostname, "claude-desktop")
129
+ self.assertEqual(result.server_name, "test_server")
130
+
131
+ # Verify configuration was written
132
+ self.assertTrue(self.temp_config_path.exists())
133
+
134
+ # Verify configuration content
135
+ with open(self.temp_config_path, 'r') as f:
136
+ config_data = json.load(f)
137
+
138
+ self.assertIn("mcpServers", config_data)
139
+ self.assertIn("test_server", config_data["mcpServers"])
140
+ self.assertEqual(config_data["mcpServers"]["test_server"]["command"], "python")
141
+
142
+ @regression_test
143
+ def test_configure_server_unknown_host_type(self):
144
+ """Test configuration with unknown host type."""
145
+ server_config_data = self.test_data_loader.load_mcp_server_config("local")
146
+ server_config = MCPServerConfig(**server_config_data)
147
+ server_config.name = "test_server"
148
+
149
+ # Clear registry to simulate unknown host type
150
+ MCPHostRegistry._strategies.clear()
151
+
152
+ result = self.manager.configure_server(
153
+ server_config=server_config,
154
+ hostname="claude-desktop"
155
+ )
156
+
157
+ self.assertIsInstance(result, ConfigurationResult)
158
+ self.assertFalse(result.success)
159
+ self.assertIsNotNone(result.error_message)
160
+ self.assertIn("Unknown host type", result.error_message)
161
+
162
+ @regression_test
163
+ def test_configure_server_validation_failure(self):
164
+ """Test configuration with validation failure."""
165
+ # Create server config that will fail validation at the strategy level
166
+ server_config_data = self.test_data_loader.load_mcp_server_config("local")
167
+ server_config = MCPServerConfig(**server_config_data)
168
+ server_config.name = "test_server"
169
+
170
+ # Override the test strategy to always fail validation
171
+ @register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
172
+ class FailingValidationStrategy(MCPHostStrategy):
173
+ def get_config_path(self):
174
+ return self.temp_config_path
175
+
176
+ def is_host_available(self):
177
+ return True
178
+
179
+ def read_configuration(self):
180
+ return HostConfiguration(servers={})
181
+
182
+ def write_configuration(self, config, no_backup=False):
183
+ return True
184
+
185
+ def validate_server_config(self, server_config):
186
+ return False # Always fail validation
187
+
188
+ result = self.manager.configure_server(
189
+ server_config=server_config,
190
+ hostname="claude-desktop"
191
+ )
192
+
193
+ self.assertIsInstance(result, ConfigurationResult)
194
+ self.assertFalse(result.success)
195
+ self.assertIsNotNone(result.error_message)
196
+ self.assertIn("Server configuration invalid", result.error_message)
197
+
198
+ @regression_test
199
+ def test_remove_server_success(self):
200
+ """Test successful server removal."""
201
+ # First configure a server
202
+ server_config_data = self.test_data_loader.load_mcp_server_config("local")
203
+ server_config = MCPServerConfig(**server_config_data)
204
+ server_config.name = "test_server"
205
+
206
+ self.manager.configure_server(
207
+ server_config=server_config,
208
+ hostname="claude-desktop"
209
+ )
210
+
211
+ # Verify server exists
212
+ with open(self.temp_config_path, 'r') as f:
213
+ config_data = json.load(f)
214
+ self.assertIn("test_server", config_data["mcpServers"])
215
+
216
+ # Remove server
217
+ result = self.manager.remove_server(
218
+ server_name="test_server",
219
+ hostname="claude-desktop"
220
+ )
221
+
222
+ self.assertIsInstance(result, ConfigurationResult)
223
+ self.assertTrue(result.success)
224
+ self.assertIsNone(result.error_message)
225
+
226
+ # Verify server was removed
227
+ with open(self.temp_config_path, 'r') as f:
228
+ config_data = json.load(f)
229
+ self.assertNotIn("test_server", config_data["mcpServers"])
230
+
231
+ @regression_test
232
+ def test_remove_server_not_found(self):
233
+ """Test removing non-existent server."""
234
+ result = self.manager.remove_server(
235
+ server_name="nonexistent_server",
236
+ hostname="claude-desktop"
237
+ )
238
+
239
+ self.assertIsInstance(result, ConfigurationResult)
240
+ self.assertFalse(result.success)
241
+ self.assertIsNotNone(result.error_message)
242
+ self.assertIn("Server 'nonexistent_server' not found", result.error_message)
243
+
244
+ @regression_test
245
+ def test_sync_environment_to_hosts_success(self):
246
+ """Test successful environment synchronization."""
247
+ from hatch.mcp_host_config.models import EnvironmentData, EnvironmentPackageEntry, PackageHostConfiguration
248
+ from datetime import datetime
249
+
250
+ # Create test environment data
251
+ server_config_data = self.test_data_loader.load_mcp_server_config("local")
252
+ server_config = MCPServerConfig(**server_config_data)
253
+
254
+ host_config = PackageHostConfiguration(
255
+ config_path="~/test/config.json",
256
+ configured_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
257
+ last_synced=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
258
+ server_config=server_config
259
+ )
260
+
261
+ package = EnvironmentPackageEntry(
262
+ name="test-package",
263
+ version="1.0.0",
264
+ type="hatch",
265
+ source="github:user/test-package",
266
+ installed_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
267
+ configured_hosts={"claude-desktop": host_config}
268
+ )
269
+
270
+ env_data = EnvironmentData(
271
+ name="test_env",
272
+ description="Test environment",
273
+ created_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
274
+ packages=[package]
275
+ )
276
+
277
+ # Sync environment to hosts
278
+ result = self.manager.sync_environment_to_hosts(
279
+ env_data=env_data,
280
+ target_hosts=["claude-desktop"]
281
+ )
282
+
283
+ self.assertIsInstance(result, SyncResult)
284
+ self.assertTrue(result.success)
285
+ self.assertEqual(result.servers_synced, 1)
286
+ self.assertEqual(result.hosts_updated, 1)
287
+ self.assertEqual(len(result.results), 1)
288
+
289
+ # Verify configuration was written
290
+ self.assertTrue(self.temp_config_path.exists())
291
+
292
+ # Verify configuration content
293
+ with open(self.temp_config_path, 'r') as f:
294
+ config_data = json.load(f)
295
+
296
+ self.assertIn("mcpServers", config_data)
297
+ self.assertIn("test-package", config_data["mcpServers"])
298
+ self.assertEqual(config_data["mcpServers"]["test-package"]["command"], "python")
299
+
300
+ @regression_test
301
+ def test_sync_environment_to_hosts_no_servers(self):
302
+ """Test environment synchronization with no servers."""
303
+ from hatch.mcp_host_config.models import EnvironmentData
304
+ from datetime import datetime
305
+
306
+ # Create empty environment data
307
+ env_data = EnvironmentData(
308
+ name="empty_env",
309
+ description="Empty environment",
310
+ created_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
311
+ packages=[]
312
+ )
313
+
314
+ # Sync environment to hosts
315
+ result = self.manager.sync_environment_to_hosts(
316
+ env_data=env_data,
317
+ target_hosts=["claude-desktop"]
318
+ )
319
+
320
+ self.assertIsInstance(result, SyncResult)
321
+ self.assertTrue(result.success) # Success even with no servers
322
+ self.assertEqual(result.servers_synced, 0)
323
+ self.assertEqual(result.hosts_updated, 1)
324
+ self.assertEqual(len(result.results), 1)
325
+
326
+ # Verify result message
327
+ self.assertEqual(result.results[0].error_message, "No servers to sync")
328
+
329
+
330
+ if __name__ == '__main__':
331
+ unittest.main()