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,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test suite for MCP environment integration.
|
|
3
|
+
|
|
4
|
+
This module tests the integration between environment data and MCP host configuration
|
|
5
|
+
with the corrected data structure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from unittest.mock import MagicMock, patch
|
|
13
|
+
import json
|
|
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.models import (
|
|
32
|
+
MCPServerConfig, EnvironmentData, EnvironmentPackageEntry,
|
|
33
|
+
PackageHostConfiguration, MCPHostType
|
|
34
|
+
)
|
|
35
|
+
from hatch.environment_manager import HatchEnvironmentManager
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestMCPEnvironmentIntegration(unittest.TestCase):
|
|
39
|
+
"""Test suite for MCP environment integration with corrected structure."""
|
|
40
|
+
|
|
41
|
+
def setUp(self):
|
|
42
|
+
"""Set up test environment."""
|
|
43
|
+
self.test_data_loader = MCPHostConfigTestDataLoader()
|
|
44
|
+
|
|
45
|
+
@regression_test
|
|
46
|
+
def test_environment_data_validation_success(self):
|
|
47
|
+
"""Test successful environment data validation."""
|
|
48
|
+
env_data = self.test_data_loader.load_corrected_environment_data("simple")
|
|
49
|
+
environment = EnvironmentData(**env_data)
|
|
50
|
+
|
|
51
|
+
self.assertEqual(environment.name, "test_environment")
|
|
52
|
+
self.assertEqual(len(environment.packages), 1)
|
|
53
|
+
|
|
54
|
+
package = environment.packages[0]
|
|
55
|
+
self.assertEqual(package.name, "weather-toolkit")
|
|
56
|
+
self.assertEqual(package.version, "1.0.0")
|
|
57
|
+
self.assertIn("claude-desktop", package.configured_hosts)
|
|
58
|
+
|
|
59
|
+
host_config = package.configured_hosts["claude-desktop"]
|
|
60
|
+
self.assertIsInstance(host_config, PackageHostConfiguration)
|
|
61
|
+
self.assertIsInstance(host_config.server_config, MCPServerConfig)
|
|
62
|
+
|
|
63
|
+
@regression_test
|
|
64
|
+
def test_environment_data_multi_host_validation(self):
|
|
65
|
+
"""Test environment data validation with multiple hosts."""
|
|
66
|
+
env_data = self.test_data_loader.load_corrected_environment_data("multi_host")
|
|
67
|
+
environment = EnvironmentData(**env_data)
|
|
68
|
+
|
|
69
|
+
self.assertEqual(environment.name, "multi_host_environment")
|
|
70
|
+
self.assertEqual(len(environment.packages), 1)
|
|
71
|
+
|
|
72
|
+
package = environment.packages[0]
|
|
73
|
+
self.assertEqual(package.name, "file-manager")
|
|
74
|
+
self.assertEqual(len(package.configured_hosts), 2)
|
|
75
|
+
self.assertIn("claude-desktop", package.configured_hosts)
|
|
76
|
+
self.assertIn("cursor", package.configured_hosts)
|
|
77
|
+
|
|
78
|
+
# Verify both host configurations
|
|
79
|
+
claude_config = package.configured_hosts["claude-desktop"]
|
|
80
|
+
cursor_config = package.configured_hosts["cursor"]
|
|
81
|
+
|
|
82
|
+
self.assertIsInstance(claude_config, PackageHostConfiguration)
|
|
83
|
+
self.assertIsInstance(cursor_config, PackageHostConfiguration)
|
|
84
|
+
|
|
85
|
+
# Verify server configurations are different for different hosts
|
|
86
|
+
self.assertEqual(claude_config.server_config.command, "/usr/local/bin/python")
|
|
87
|
+
self.assertEqual(cursor_config.server_config.command, "python")
|
|
88
|
+
|
|
89
|
+
@regression_test
|
|
90
|
+
def test_package_host_configuration_validation(self):
|
|
91
|
+
"""Test package host configuration validation."""
|
|
92
|
+
server_config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
93
|
+
server_config = MCPServerConfig(**server_config_data)
|
|
94
|
+
|
|
95
|
+
host_config = PackageHostConfiguration(
|
|
96
|
+
config_path="~/test/config.json",
|
|
97
|
+
configured_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
98
|
+
last_synced=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
99
|
+
server_config=server_config
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
self.assertEqual(host_config.config_path, "~/test/config.json")
|
|
103
|
+
self.assertIsInstance(host_config.server_config, MCPServerConfig)
|
|
104
|
+
self.assertEqual(host_config.server_config.command, "python")
|
|
105
|
+
self.assertEqual(len(host_config.server_config.args), 3)
|
|
106
|
+
|
|
107
|
+
@regression_test
|
|
108
|
+
def test_environment_package_entry_validation_success(self):
|
|
109
|
+
"""Test successful environment package entry validation."""
|
|
110
|
+
server_config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
111
|
+
server_config = MCPServerConfig(**server_config_data)
|
|
112
|
+
|
|
113
|
+
host_config = PackageHostConfiguration(
|
|
114
|
+
config_path="~/test/config.json",
|
|
115
|
+
configured_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
116
|
+
last_synced=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
117
|
+
server_config=server_config
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
package = EnvironmentPackageEntry(
|
|
121
|
+
name="test-package",
|
|
122
|
+
version="1.0.0",
|
|
123
|
+
type="hatch",
|
|
124
|
+
source="github:user/test-package",
|
|
125
|
+
installed_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
126
|
+
configured_hosts={"claude-desktop": host_config}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.assertEqual(package.name, "test-package")
|
|
130
|
+
self.assertEqual(package.version, "1.0.0")
|
|
131
|
+
self.assertEqual(package.type, "hatch")
|
|
132
|
+
self.assertEqual(len(package.configured_hosts), 1)
|
|
133
|
+
self.assertIn("claude-desktop", package.configured_hosts)
|
|
134
|
+
|
|
135
|
+
@regression_test
|
|
136
|
+
def test_environment_package_entry_invalid_host_name(self):
|
|
137
|
+
"""Test environment package entry validation with invalid host name."""
|
|
138
|
+
server_config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
139
|
+
server_config = MCPServerConfig(**server_config_data)
|
|
140
|
+
|
|
141
|
+
host_config = PackageHostConfiguration(
|
|
142
|
+
config_path="~/test/config.json",
|
|
143
|
+
configured_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
144
|
+
last_synced=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
145
|
+
server_config=server_config
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
with self.assertRaises(Exception) as context:
|
|
149
|
+
EnvironmentPackageEntry(
|
|
150
|
+
name="test-package",
|
|
151
|
+
version="1.0.0",
|
|
152
|
+
type="hatch",
|
|
153
|
+
source="github:user/test-package",
|
|
154
|
+
installed_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
155
|
+
configured_hosts={"invalid-host": host_config} # Invalid host name
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
self.assertIn("Unsupported host", str(context.exception))
|
|
159
|
+
|
|
160
|
+
@regression_test
|
|
161
|
+
def test_environment_package_entry_invalid_package_name(self):
|
|
162
|
+
"""Test environment package entry validation with invalid package name."""
|
|
163
|
+
server_config_data = self.test_data_loader.load_mcp_server_config("local")
|
|
164
|
+
server_config = MCPServerConfig(**server_config_data)
|
|
165
|
+
|
|
166
|
+
host_config = PackageHostConfiguration(
|
|
167
|
+
config_path="~/test/config.json",
|
|
168
|
+
configured_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
169
|
+
last_synced=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
170
|
+
server_config=server_config
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
with self.assertRaises(Exception) as context:
|
|
174
|
+
EnvironmentPackageEntry(
|
|
175
|
+
name="invalid@package!name", # Invalid characters
|
|
176
|
+
version="1.0.0",
|
|
177
|
+
type="hatch",
|
|
178
|
+
source="github:user/test-package",
|
|
179
|
+
installed_at=datetime.fromisoformat("2025-09-21T10:00:00.000000"),
|
|
180
|
+
configured_hosts={"claude-desktop": host_config}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self.assertIn("Invalid package name format", str(context.exception))
|
|
184
|
+
|
|
185
|
+
@regression_test
|
|
186
|
+
def test_environment_data_get_mcp_packages(self):
|
|
187
|
+
"""Test getting MCP packages from environment data."""
|
|
188
|
+
env_data = self.test_data_loader.load_corrected_environment_data("multi_host")
|
|
189
|
+
environment = EnvironmentData(**env_data)
|
|
190
|
+
|
|
191
|
+
mcp_packages = environment.get_mcp_packages()
|
|
192
|
+
|
|
193
|
+
self.assertEqual(len(mcp_packages), 1)
|
|
194
|
+
self.assertEqual(mcp_packages[0].name, "file-manager")
|
|
195
|
+
self.assertEqual(len(mcp_packages[0].configured_hosts), 2)
|
|
196
|
+
|
|
197
|
+
@regression_test
|
|
198
|
+
def test_environment_data_serialization_roundtrip(self):
|
|
199
|
+
"""Test environment data serialization and deserialization."""
|
|
200
|
+
env_data = self.test_data_loader.load_corrected_environment_data("simple")
|
|
201
|
+
environment = EnvironmentData(**env_data)
|
|
202
|
+
|
|
203
|
+
# Serialize and deserialize
|
|
204
|
+
serialized = environment.model_dump()
|
|
205
|
+
roundtrip_environment = EnvironmentData(**serialized)
|
|
206
|
+
|
|
207
|
+
self.assertEqual(environment.name, roundtrip_environment.name)
|
|
208
|
+
self.assertEqual(len(environment.packages), len(roundtrip_environment.packages))
|
|
209
|
+
|
|
210
|
+
original_package = environment.packages[0]
|
|
211
|
+
roundtrip_package = roundtrip_environment.packages[0]
|
|
212
|
+
|
|
213
|
+
self.assertEqual(original_package.name, roundtrip_package.name)
|
|
214
|
+
self.assertEqual(original_package.version, roundtrip_package.version)
|
|
215
|
+
self.assertEqual(len(original_package.configured_hosts), len(roundtrip_package.configured_hosts))
|
|
216
|
+
|
|
217
|
+
# Verify host configuration roundtrip
|
|
218
|
+
original_host_config = original_package.configured_hosts["claude-desktop"]
|
|
219
|
+
roundtrip_host_config = roundtrip_package.configured_hosts["claude-desktop"]
|
|
220
|
+
|
|
221
|
+
self.assertEqual(original_host_config.config_path, roundtrip_host_config.config_path)
|
|
222
|
+
self.assertEqual(original_host_config.server_config.command, roundtrip_host_config.server_config.command)
|
|
223
|
+
|
|
224
|
+
@regression_test
|
|
225
|
+
def test_corrected_environment_structure_single_server_per_package(self):
|
|
226
|
+
"""Test that corrected environment structure enforces single server per package."""
|
|
227
|
+
env_data = self.test_data_loader.load_corrected_environment_data("simple")
|
|
228
|
+
environment = EnvironmentData(**env_data)
|
|
229
|
+
|
|
230
|
+
# Verify single server per package constraint
|
|
231
|
+
for package in environment.packages:
|
|
232
|
+
# Each package should have one server configuration per host
|
|
233
|
+
for host_name, host_config in package.configured_hosts.items():
|
|
234
|
+
self.assertIsInstance(host_config, PackageHostConfiguration)
|
|
235
|
+
self.assertIsInstance(host_config.server_config, MCPServerConfig)
|
|
236
|
+
|
|
237
|
+
# The server configuration should be for this specific package
|
|
238
|
+
# (In real usage, the server would be the package's MCP server)
|
|
239
|
+
|
|
240
|
+
@regression_test
|
|
241
|
+
def test_environment_data_json_serialization(self):
|
|
242
|
+
"""Test JSON serialization compatibility."""
|
|
243
|
+
import json
|
|
244
|
+
|
|
245
|
+
env_data = self.test_data_loader.load_corrected_environment_data("simple")
|
|
246
|
+
environment = EnvironmentData(**env_data)
|
|
247
|
+
|
|
248
|
+
# Test JSON serialization
|
|
249
|
+
json_str = environment.model_dump_json()
|
|
250
|
+
self.assertIsInstance(json_str, str)
|
|
251
|
+
|
|
252
|
+
# Test JSON deserialization
|
|
253
|
+
parsed_data = json.loads(json_str)
|
|
254
|
+
roundtrip_environment = EnvironmentData(**parsed_data)
|
|
255
|
+
|
|
256
|
+
self.assertEqual(environment.name, roundtrip_environment.name)
|
|
257
|
+
self.assertEqual(len(environment.packages), len(roundtrip_environment.packages))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class TestMCPHostTypeIntegration(unittest.TestCase):
|
|
261
|
+
"""Test suite for MCP host type integration."""
|
|
262
|
+
|
|
263
|
+
@regression_test
|
|
264
|
+
def test_mcp_host_type_enum_values(self):
|
|
265
|
+
"""Test MCP host type enum values."""
|
|
266
|
+
# Verify all expected host types are available
|
|
267
|
+
expected_hosts = [
|
|
268
|
+
"claude-desktop", "claude-code", "vscode",
|
|
269
|
+
"cursor", "lmstudio", "gemini"
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
for host_name in expected_hosts:
|
|
273
|
+
host_type = MCPHostType(host_name)
|
|
274
|
+
self.assertEqual(host_type.value, host_name)
|
|
275
|
+
|
|
276
|
+
@regression_test
|
|
277
|
+
def test_mcp_host_type_invalid_value(self):
|
|
278
|
+
"""Test MCP host type with invalid value."""
|
|
279
|
+
with self.assertRaises(ValueError):
|
|
280
|
+
MCPHostType("invalid-host")
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestEnvironmentManagerHostSync(unittest.TestCase):
|
|
284
|
+
"""Test suite for EnvironmentManager host synchronization methods."""
|
|
285
|
+
|
|
286
|
+
def setUp(self):
|
|
287
|
+
"""Set up test fixtures."""
|
|
288
|
+
self.mock_env_manager = MagicMock(spec=HatchEnvironmentManager)
|
|
289
|
+
|
|
290
|
+
# Load test fixture data
|
|
291
|
+
fixture_path = Path(__file__).parent / "test_data" / "fixtures" / "host_sync_scenarios.json"
|
|
292
|
+
with open(fixture_path, 'r') as f:
|
|
293
|
+
self.test_data = json.load(f)
|
|
294
|
+
|
|
295
|
+
@regression_test
|
|
296
|
+
def test_remove_package_host_configuration_success(self):
|
|
297
|
+
"""Test successful removal of host from package tracking.
|
|
298
|
+
|
|
299
|
+
Validates:
|
|
300
|
+
- Removes specified host from package's configured_hosts
|
|
301
|
+
- Updates environments.json file via _save_environments()
|
|
302
|
+
- Returns True when removal occurs
|
|
303
|
+
- Logs successful removal with package/host details
|
|
304
|
+
"""
|
|
305
|
+
# Setup: Environment with package having configured_hosts for multiple hosts
|
|
306
|
+
env_manager = HatchEnvironmentManager()
|
|
307
|
+
env_manager._environments = {
|
|
308
|
+
"test-env": self.test_data["remove_server_scenario"]["before"]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
with patch.object(env_manager, '_save_environments') as mock_save:
|
|
312
|
+
with patch.object(env_manager, 'logger') as mock_logger:
|
|
313
|
+
# Action: remove_package_host_configuration(env_name, package_name, hostname)
|
|
314
|
+
result = env_manager.remove_package_host_configuration("test-env", "weather-toolkit", "cursor")
|
|
315
|
+
|
|
316
|
+
# Assert: Host removed from package, environments.json updated, returns True
|
|
317
|
+
self.assertTrue(result)
|
|
318
|
+
mock_save.assert_called_once()
|
|
319
|
+
mock_logger.info.assert_called_with("Removed host cursor from package weather-toolkit in env test-env")
|
|
320
|
+
|
|
321
|
+
# Verify host was actually removed
|
|
322
|
+
packages = env_manager._environments["test-env"]["packages"]
|
|
323
|
+
weather_pkg = next(pkg for pkg in packages if pkg["name"] == "weather-toolkit")
|
|
324
|
+
self.assertNotIn("cursor", weather_pkg["configured_hosts"])
|
|
325
|
+
self.assertIn("claude-desktop", weather_pkg["configured_hosts"])
|
|
326
|
+
|
|
327
|
+
@regression_test
|
|
328
|
+
def test_remove_package_host_configuration_not_found(self):
|
|
329
|
+
"""Test removal when package or host not found.
|
|
330
|
+
|
|
331
|
+
Validates:
|
|
332
|
+
- Returns False when environment doesn't exist
|
|
333
|
+
- Returns False when package not found in environment
|
|
334
|
+
- Returns False when host not in package's configured_hosts
|
|
335
|
+
- No changes to environments.json when nothing to remove
|
|
336
|
+
"""
|
|
337
|
+
env_manager = HatchEnvironmentManager()
|
|
338
|
+
env_manager._environments = {
|
|
339
|
+
"test-env": self.test_data["remove_server_scenario"]["before"]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
with patch.object(env_manager, '_save_environments') as mock_save:
|
|
343
|
+
# Test scenarios: missing env, missing package, missing host
|
|
344
|
+
|
|
345
|
+
# Missing environment
|
|
346
|
+
result = env_manager.remove_package_host_configuration("missing-env", "weather-toolkit", "cursor")
|
|
347
|
+
self.assertFalse(result)
|
|
348
|
+
|
|
349
|
+
# Missing package
|
|
350
|
+
result = env_manager.remove_package_host_configuration("test-env", "missing-package", "cursor")
|
|
351
|
+
self.assertFalse(result)
|
|
352
|
+
|
|
353
|
+
# Missing host
|
|
354
|
+
result = env_manager.remove_package_host_configuration("test-env", "weather-toolkit", "missing-host")
|
|
355
|
+
self.assertFalse(result)
|
|
356
|
+
|
|
357
|
+
# Assert: No file changes when nothing to remove
|
|
358
|
+
mock_save.assert_not_called()
|
|
359
|
+
|
|
360
|
+
@regression_test
|
|
361
|
+
def test_clear_host_from_all_packages_all_envs(self):
|
|
362
|
+
"""Test host removal across multiple environments.
|
|
363
|
+
|
|
364
|
+
Validates:
|
|
365
|
+
- Iterates through all environments in _environments
|
|
366
|
+
- Removes hostname from all packages' configured_hosts
|
|
367
|
+
- Returns correct count of updated package entries
|
|
368
|
+
- Calls _save_environments() only once after all updates
|
|
369
|
+
"""
|
|
370
|
+
# Setup: Multiple environments with packages using same host
|
|
371
|
+
env_manager = HatchEnvironmentManager()
|
|
372
|
+
env_manager._environments = self.test_data["remove_host_scenario"]["multi_environment_before"]
|
|
373
|
+
|
|
374
|
+
with patch.object(env_manager, '_save_environments') as mock_save:
|
|
375
|
+
with patch.object(env_manager, 'logger') as mock_logger:
|
|
376
|
+
# Action: clear_host_from_all_packages_all_envs(hostname)
|
|
377
|
+
updates_count = env_manager.clear_host_from_all_packages_all_envs("cursor")
|
|
378
|
+
|
|
379
|
+
# Assert: Host removed from all packages, correct count returned
|
|
380
|
+
self.assertEqual(updates_count, 2) # 2 packages had cursor configured
|
|
381
|
+
mock_save.assert_called_once()
|
|
382
|
+
|
|
383
|
+
# Verify cursor was removed from all packages
|
|
384
|
+
for env_name, env_data in env_manager._environments.items():
|
|
385
|
+
for pkg in env_data["packages"]:
|
|
386
|
+
configured_hosts = pkg.get("configured_hosts", {})
|
|
387
|
+
self.assertNotIn("cursor", configured_hosts)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestEnvironmentManagerHostSyncErrorHandling(unittest.TestCase):
|
|
391
|
+
"""Test suite for error handling and edge cases."""
|
|
392
|
+
|
|
393
|
+
def setUp(self):
|
|
394
|
+
"""Set up test fixtures."""
|
|
395
|
+
self.env_manager = HatchEnvironmentManager()
|
|
396
|
+
|
|
397
|
+
@regression_test
|
|
398
|
+
def test_remove_operations_exception_handling(self):
|
|
399
|
+
"""Test exception handling in remove operations.
|
|
400
|
+
|
|
401
|
+
Validates:
|
|
402
|
+
- Catches and logs exceptions during removal operations
|
|
403
|
+
- Returns False/0 on exceptions rather than crashing
|
|
404
|
+
- Provides meaningful error messages in logs
|
|
405
|
+
- Maintains environment file integrity on errors
|
|
406
|
+
"""
|
|
407
|
+
# Setup: Mock scenarios that raise exceptions
|
|
408
|
+
# Create environment with package that has the host, so _save_environments will be called
|
|
409
|
+
self.env_manager._environments = {
|
|
410
|
+
"test-env": {
|
|
411
|
+
"packages": [
|
|
412
|
+
{
|
|
413
|
+
"name": "test-pkg",
|
|
414
|
+
"configured_hosts": {
|
|
415
|
+
"test-host": {"config_path": "test"}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
with patch.object(self.env_manager, '_save_environments', side_effect=Exception("File error")):
|
|
423
|
+
with patch.object(self.env_manager, 'logger') as mock_logger:
|
|
424
|
+
# Action: Call remove methods with exception-inducing conditions
|
|
425
|
+
result = self.env_manager.remove_package_host_configuration("test-env", "test-pkg", "test-host")
|
|
426
|
+
|
|
427
|
+
# Assert: Graceful error handling, no crashes, appropriate returns
|
|
428
|
+
self.assertFalse(result)
|
|
429
|
+
mock_logger.error.assert_called()
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestCLIHostMutationSync(unittest.TestCase):
|
|
433
|
+
"""Test suite for CLI integration with environment tracking."""
|
|
434
|
+
|
|
435
|
+
def setUp(self):
|
|
436
|
+
"""Set up test fixtures."""
|
|
437
|
+
self.mock_env_manager = MagicMock(spec=HatchEnvironmentManager)
|
|
438
|
+
|
|
439
|
+
@integration_test(scope="component")
|
|
440
|
+
def test_remove_server_updates_environment(self):
|
|
441
|
+
"""Test that remove server updates current environment tracking.
|
|
442
|
+
|
|
443
|
+
Validates:
|
|
444
|
+
- CLI remove server calls environment manager update method
|
|
445
|
+
- Updates only current environment (not all environments)
|
|
446
|
+
- Passes correct parameters (env_name, server_name, hostname)
|
|
447
|
+
- Maintains existing CLI behavior and exit codes
|
|
448
|
+
"""
|
|
449
|
+
from hatch.cli_hatch import handle_mcp_remove_server
|
|
450
|
+
from hatch.mcp_host_config import MCPHostConfigurationManager
|
|
451
|
+
|
|
452
|
+
# Setup: Environment with server configured on host
|
|
453
|
+
self.mock_env_manager.get_current_environment.return_value = "test-env"
|
|
454
|
+
|
|
455
|
+
with patch.object(MCPHostConfigurationManager, 'remove_server') as mock_remove:
|
|
456
|
+
mock_result = MagicMock()
|
|
457
|
+
mock_result.success = True
|
|
458
|
+
mock_result.backup_path = None
|
|
459
|
+
mock_remove.return_value = mock_result
|
|
460
|
+
|
|
461
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
462
|
+
with patch('builtins.print'):
|
|
463
|
+
# Action: hatch mcp remove server <server> --host <host>
|
|
464
|
+
result = handle_mcp_remove_server(
|
|
465
|
+
self.mock_env_manager, "test-server", "claude-desktop",
|
|
466
|
+
None, False, False, True
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Assert: Environment manager method called with correct parameters
|
|
470
|
+
self.mock_env_manager.get_current_environment.assert_called_once()
|
|
471
|
+
self.mock_env_manager.remove_package_host_configuration.assert_called_with(
|
|
472
|
+
"test-env", "test-server", "claude-desktop"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# Assert: Success exit code
|
|
476
|
+
self.assertEqual(result, 0)
|
|
477
|
+
|
|
478
|
+
@integration_test(scope="component")
|
|
479
|
+
def test_remove_host_updates_all_environments(self):
|
|
480
|
+
"""Test that remove host updates all environment tracking.
|
|
481
|
+
|
|
482
|
+
Validates:
|
|
483
|
+
- CLI remove host calls global environment update method
|
|
484
|
+
- Updates ALL environments (not just current)
|
|
485
|
+
- Passes correct hostname parameter
|
|
486
|
+
- Reports number of updates performed to user
|
|
487
|
+
"""
|
|
488
|
+
from hatch.cli_hatch import handle_mcp_remove_host
|
|
489
|
+
from hatch.mcp_host_config import MCPHostConfigurationManager
|
|
490
|
+
|
|
491
|
+
# Setup: Multiple environments with packages using the host
|
|
492
|
+
with patch.object(MCPHostConfigurationManager, 'remove_host_configuration') as mock_remove:
|
|
493
|
+
mock_result = MagicMock()
|
|
494
|
+
mock_result.success = True
|
|
495
|
+
mock_result.backup_path = None
|
|
496
|
+
mock_remove.return_value = mock_result
|
|
497
|
+
|
|
498
|
+
self.mock_env_manager.clear_host_from_all_packages_all_envs.return_value = 3
|
|
499
|
+
|
|
500
|
+
with patch('hatch.cli_hatch.request_confirmation', return_value=True):
|
|
501
|
+
with patch('builtins.print') as mock_print:
|
|
502
|
+
# Action: hatch mcp remove host <host>
|
|
503
|
+
result = handle_mcp_remove_host(
|
|
504
|
+
self.mock_env_manager, "cursor", False, False, True
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Assert: Global environment update method called
|
|
508
|
+
self.mock_env_manager.clear_host_from_all_packages_all_envs.assert_called_with("cursor")
|
|
509
|
+
|
|
510
|
+
# Assert: User informed of update count
|
|
511
|
+
print_calls = [call[0][0] for call in mock_print.call_args_list]
|
|
512
|
+
output = ' '.join(print_calls)
|
|
513
|
+
self.assertIn("Updated 3 package entries across environments", output)
|
|
514
|
+
|
|
515
|
+
# Assert: Success exit code
|
|
516
|
+
self.assertEqual(result, 0)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == '__main__':
|
|
520
|
+
unittest.main()
|