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
tests/test_env_manip.py
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
import logging
|
|
5
|
+
import tempfile
|
|
6
|
+
import shutil
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from unittest.mock import patch
|
|
11
|
+
|
|
12
|
+
from wobble.decorators import regression_test, integration_test, slow_test
|
|
13
|
+
|
|
14
|
+
# Import path management removed - using test_data_utils for test dependencies
|
|
15
|
+
|
|
16
|
+
from hatch.environment_manager import HatchEnvironmentManager
|
|
17
|
+
from hatch.installers.docker_installer import DOCKER_DAEMON_AVAILABLE
|
|
18
|
+
|
|
19
|
+
# Configure logging
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
23
|
+
)
|
|
24
|
+
logger = logging.getLogger("hatch.environment_tests")
|
|
25
|
+
|
|
26
|
+
class PackageEnvironmentTests(unittest.TestCase):
|
|
27
|
+
"""Tests for the package environment management functionality."""
|
|
28
|
+
|
|
29
|
+
def setUp(self):
|
|
30
|
+
"""Set up test environment before each test."""
|
|
31
|
+
# Create a temporary directory for test environments
|
|
32
|
+
self.temp_dir = tempfile.mkdtemp()
|
|
33
|
+
|
|
34
|
+
# Path to Hatching-Dev packages
|
|
35
|
+
self.hatch_dev_path = Path(__file__).parent.parent.parent / "Hatching-Dev"
|
|
36
|
+
self.assertTrue(self.hatch_dev_path.exists(),
|
|
37
|
+
f"Hatching-Dev directory not found at {self.hatch_dev_path}")
|
|
38
|
+
|
|
39
|
+
# Create a sample registry that includes Hatching-Dev packages
|
|
40
|
+
self._create_sample_registry()
|
|
41
|
+
|
|
42
|
+
# Override environment paths to use our test directory
|
|
43
|
+
env_dir = Path(self.temp_dir) / "envs"
|
|
44
|
+
env_dir.mkdir(exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# Create environment manager for testing with isolated test directories
|
|
47
|
+
self.env_manager = HatchEnvironmentManager(
|
|
48
|
+
environments_dir=env_dir,
|
|
49
|
+
simulation_mode=True,
|
|
50
|
+
local_registry_cache_path=self.registry_path)
|
|
51
|
+
|
|
52
|
+
# Reload environments to ensure clean state
|
|
53
|
+
self.env_manager.reload_environments()
|
|
54
|
+
|
|
55
|
+
def _create_sample_registry(self):
|
|
56
|
+
"""Create a sample registry with Hatching-Dev packages using real metadata."""
|
|
57
|
+
now = datetime.now().isoformat()
|
|
58
|
+
registry = {
|
|
59
|
+
"registry_schema_version": "1.1.0",
|
|
60
|
+
"last_updated": now,
|
|
61
|
+
"repositories": [
|
|
62
|
+
{
|
|
63
|
+
"name": "test-repo",
|
|
64
|
+
"url": f"file://{self.hatch_dev_path}",
|
|
65
|
+
"last_indexed": now,
|
|
66
|
+
"packages": []
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"stats": {
|
|
70
|
+
"total_packages": 0,
|
|
71
|
+
"total_versions": 0
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
# Use self-contained test packages instead of external Hatching-Dev
|
|
75
|
+
from test_data_utils import TestDataLoader
|
|
76
|
+
test_loader = TestDataLoader()
|
|
77
|
+
|
|
78
|
+
pkg_names = [
|
|
79
|
+
"base_pkg", "utility_pkg", "python_dep_pkg",
|
|
80
|
+
"circular_dep_pkg", "circular_dep_pkg_b", "complex_dep_pkg", "simple_dep_pkg"
|
|
81
|
+
]
|
|
82
|
+
for pkg_name in pkg_names:
|
|
83
|
+
# Map to self-contained package locations
|
|
84
|
+
if pkg_name in ["base_pkg", "utility_pkg"]:
|
|
85
|
+
pkg_path = test_loader.packages_dir / "basic" / pkg_name
|
|
86
|
+
elif pkg_name in ["complex_dep_pkg", "simple_dep_pkg", "python_dep_pkg"]:
|
|
87
|
+
pkg_path = test_loader.packages_dir / "dependencies" / pkg_name
|
|
88
|
+
elif pkg_name in ["circular_dep_pkg", "circular_dep_pkg_b"]:
|
|
89
|
+
pkg_path = test_loader.packages_dir / "error_scenarios" / pkg_name
|
|
90
|
+
else:
|
|
91
|
+
pkg_path = test_loader.packages_dir / pkg_name
|
|
92
|
+
if pkg_path.exists():
|
|
93
|
+
metadata_path = pkg_path / "hatch_metadata.json"
|
|
94
|
+
if metadata_path.exists():
|
|
95
|
+
try:
|
|
96
|
+
with open(metadata_path, 'r') as f:
|
|
97
|
+
metadata = json.load(f)
|
|
98
|
+
pkg_entry = {
|
|
99
|
+
"name": metadata.get("name", pkg_name),
|
|
100
|
+
"description": metadata.get("description", ""),
|
|
101
|
+
"tags": metadata.get("tags", []),
|
|
102
|
+
"latest_version": metadata.get("version", "1.0.0"),
|
|
103
|
+
"versions": [
|
|
104
|
+
{
|
|
105
|
+
"version": metadata.get("version", "1.0.0"),
|
|
106
|
+
"release_uri": f"file://{pkg_path}",
|
|
107
|
+
"author": {
|
|
108
|
+
"GitHubID": metadata.get("author", {}).get("name", "test_user"),
|
|
109
|
+
"email": metadata.get("author", {}).get("email", "test@example.com")
|
|
110
|
+
},
|
|
111
|
+
"added_date": now,
|
|
112
|
+
"hatch_dependencies_added": [
|
|
113
|
+
{
|
|
114
|
+
"name": dep["name"],
|
|
115
|
+
"version_constraint": dep.get("version_constraint", "")
|
|
116
|
+
} for dep in metadata.get("dependencies", {}).get("hatch", [])
|
|
117
|
+
],
|
|
118
|
+
"python_dependencies_added": [
|
|
119
|
+
{
|
|
120
|
+
"name": dep["name"],
|
|
121
|
+
"version_constraint": dep.get("version_constraint", ""),
|
|
122
|
+
"package_manager": dep.get("package_manager", "pip")
|
|
123
|
+
} for dep in metadata.get("dependencies", {}).get("python", [])
|
|
124
|
+
],
|
|
125
|
+
"hatch_dependencies_removed": [],
|
|
126
|
+
"hatch_dependencies_modified": [],
|
|
127
|
+
"python_dependencies_removed": [],
|
|
128
|
+
"python_dependencies_modified": [],
|
|
129
|
+
"compatibility_changes": {}
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
registry["repositories"][0]["packages"].append(pkg_entry)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Failed to load metadata for {pkg_name}: {e}")
|
|
136
|
+
raise e
|
|
137
|
+
# Update stats
|
|
138
|
+
registry["stats"]["total_packages"] = len(registry["repositories"][0]["packages"])
|
|
139
|
+
registry["stats"]["total_versions"] = sum(len(pkg["versions"]) for pkg in registry["repositories"][0]["packages"])
|
|
140
|
+
registry_dir = Path(self.temp_dir) / "registry"
|
|
141
|
+
registry_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
self.registry_path = registry_dir / "hatch_packages_registry.json"
|
|
143
|
+
with open(self.registry_path, "w") as f:
|
|
144
|
+
json.dump(registry, f, indent=2)
|
|
145
|
+
logger.info(f"Sample registry created at {self.registry_path}")
|
|
146
|
+
|
|
147
|
+
def tearDown(self):
|
|
148
|
+
"""Clean up test environment after each test."""
|
|
149
|
+
# Remove temporary directory
|
|
150
|
+
shutil.rmtree(self.temp_dir)
|
|
151
|
+
|
|
152
|
+
@regression_test
|
|
153
|
+
@slow_test
|
|
154
|
+
def test_create_environment(self):
|
|
155
|
+
"""Test creating an environment."""
|
|
156
|
+
result = self.env_manager.create_environment("test_env", "Test environment")
|
|
157
|
+
self.assertTrue(result, "Failed to create environment")
|
|
158
|
+
|
|
159
|
+
# Verify environment exists
|
|
160
|
+
self.assertTrue(self.env_manager.environment_exists("test_env"), "Environment doesn't exist after creation")
|
|
161
|
+
|
|
162
|
+
# Verify environment data
|
|
163
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
164
|
+
self.assertIsNotNone(env_data, "Environment data not found")
|
|
165
|
+
self.assertEqual(env_data["name"], "test_env")
|
|
166
|
+
self.assertEqual(env_data["description"], "Test environment")
|
|
167
|
+
self.assertIn("created_at", env_data)
|
|
168
|
+
self.assertIn("packages", env_data)
|
|
169
|
+
self.assertEqual(len(env_data["packages"]), 0)
|
|
170
|
+
|
|
171
|
+
@regression_test
|
|
172
|
+
@slow_test
|
|
173
|
+
def test_remove_environment(self):
|
|
174
|
+
"""Test removing an environment."""
|
|
175
|
+
# First create an environment
|
|
176
|
+
self.env_manager.create_environment("test_env", "Test environment")
|
|
177
|
+
self.assertTrue(self.env_manager.environment_exists("test_env"))
|
|
178
|
+
|
|
179
|
+
# Then remove it
|
|
180
|
+
result = self.env_manager.remove_environment("test_env")
|
|
181
|
+
self.assertTrue(result, "Failed to remove environment")
|
|
182
|
+
|
|
183
|
+
# Verify environment no longer exists
|
|
184
|
+
self.assertFalse(self.env_manager.environment_exists("test_env"), "Environment still exists after removal")
|
|
185
|
+
|
|
186
|
+
@regression_test
|
|
187
|
+
@slow_test
|
|
188
|
+
def test_set_current_environment(self):
|
|
189
|
+
"""Test setting the current environment."""
|
|
190
|
+
# First create an environment
|
|
191
|
+
self.env_manager.create_environment("test_env", "Test environment")
|
|
192
|
+
|
|
193
|
+
# Set it as current
|
|
194
|
+
result = self.env_manager.set_current_environment("test_env")
|
|
195
|
+
self.assertTrue(result, "Failed to set current environment")
|
|
196
|
+
|
|
197
|
+
# Verify it's the current environment
|
|
198
|
+
current_env = self.env_manager.get_current_environment()
|
|
199
|
+
self.assertEqual(current_env, "test_env", "Current environment not set correctly")
|
|
200
|
+
|
|
201
|
+
@regression_test
|
|
202
|
+
@slow_test
|
|
203
|
+
def test_add_local_package(self):
|
|
204
|
+
"""Test adding a local package to an environment."""
|
|
205
|
+
# Create an environment
|
|
206
|
+
self.env_manager.create_environment("test_env", "Test environment")
|
|
207
|
+
self.env_manager.set_current_environment("test_env")
|
|
208
|
+
|
|
209
|
+
# Use base_pkg from self-contained test data
|
|
210
|
+
from test_data_utils import TestDataLoader
|
|
211
|
+
test_loader = TestDataLoader()
|
|
212
|
+
pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
213
|
+
self.assertTrue(pkg_path.exists(), f"Test package not found: {pkg_path}")
|
|
214
|
+
|
|
215
|
+
# Add package to environment
|
|
216
|
+
result = self.env_manager.add_package_to_environment(
|
|
217
|
+
str(pkg_path), # Convert to string to handle Path objects
|
|
218
|
+
"test_env",
|
|
219
|
+
auto_approve=True # Auto-approve for testing
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self.assertTrue(result, "Failed to add local package to environment")
|
|
223
|
+
|
|
224
|
+
# Verify package was added to environment data
|
|
225
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
226
|
+
self.assertIsNotNone(env_data, "Environment data not found")
|
|
227
|
+
|
|
228
|
+
packages = env_data.get("packages", [])
|
|
229
|
+
self.assertEqual(len(packages), 1, "Package not added to environment data")
|
|
230
|
+
|
|
231
|
+
pkg_data = packages[0]
|
|
232
|
+
self.assertIn("name", pkg_data, "Package data missing name")
|
|
233
|
+
self.assertIn("version", pkg_data, "Package data missing version")
|
|
234
|
+
self.assertIn("type", pkg_data, "Package data missing type")
|
|
235
|
+
self.assertIn("source", pkg_data, "Package data missing source")
|
|
236
|
+
|
|
237
|
+
@regression_test
|
|
238
|
+
@slow_test
|
|
239
|
+
def test_add_package_with_dependencies(self):
|
|
240
|
+
"""Test adding a package with dependencies to an environment."""
|
|
241
|
+
# Create an environment
|
|
242
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
243
|
+
self.env_manager.set_current_environment("test_env")
|
|
244
|
+
|
|
245
|
+
# First add the base package that is a dependency
|
|
246
|
+
from test_data_utils import TestDataLoader
|
|
247
|
+
test_loader = TestDataLoader()
|
|
248
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
249
|
+
self.assertTrue(base_pkg_path.exists(), f"Base package not found: {base_pkg_path}")
|
|
250
|
+
|
|
251
|
+
result = self.env_manager.add_package_to_environment(
|
|
252
|
+
str(base_pkg_path),
|
|
253
|
+
"test_env",
|
|
254
|
+
auto_approve=True # Auto-approve for testing
|
|
255
|
+
)
|
|
256
|
+
self.assertTrue(result, "Failed to add base package to environment")
|
|
257
|
+
|
|
258
|
+
# Then add the package with dependencies
|
|
259
|
+
pkg_path = test_loader.packages_dir / "dependencies" / "simple_dep_pkg"
|
|
260
|
+
self.assertTrue(pkg_path.exists(), f"Dependent package not found: {pkg_path}")
|
|
261
|
+
|
|
262
|
+
# Add package to environment
|
|
263
|
+
result = self.env_manager.add_package_to_environment(
|
|
264
|
+
str(pkg_path),
|
|
265
|
+
"test_env",
|
|
266
|
+
auto_approve=True # Auto-approve for testing
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
self.assertTrue(result, "Failed to add package with dependencies")
|
|
270
|
+
|
|
271
|
+
# Verify both packages are in the environment
|
|
272
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
273
|
+
self.assertIsNotNone(env_data, "Environment data not found")
|
|
274
|
+
|
|
275
|
+
packages = env_data.get("packages", [])
|
|
276
|
+
self.assertEqual(len(packages), 2, "Not all packages were added to environment")
|
|
277
|
+
|
|
278
|
+
# Check that both packages are in the environment data
|
|
279
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
280
|
+
self.assertIn("base_pkg", package_names, "Base package missing from environment")
|
|
281
|
+
self.assertIn("simple_dep_pkg", package_names, "Dependent package missing from environment")
|
|
282
|
+
|
|
283
|
+
@regression_test
|
|
284
|
+
@slow_test
|
|
285
|
+
def test_add_package_with_some_dependencies_already_present(self):
|
|
286
|
+
"""Test adding a package where some dependencies are already present and others are not."""
|
|
287
|
+
# Create an environment
|
|
288
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
289
|
+
self.env_manager.set_current_environment("test_env")
|
|
290
|
+
# First add only one of the dependencies that complex_dep_pkg needs
|
|
291
|
+
from test_data_utils import TestDataLoader
|
|
292
|
+
test_loader = TestDataLoader()
|
|
293
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
294
|
+
self.assertTrue(base_pkg_path.exists(), f"Base package not found: {base_pkg_path}")
|
|
295
|
+
|
|
296
|
+
result = self.env_manager.add_package_to_environment(
|
|
297
|
+
str(base_pkg_path),
|
|
298
|
+
"test_env",
|
|
299
|
+
auto_approve=True # Auto-approve for testing
|
|
300
|
+
)
|
|
301
|
+
self.assertTrue(result, "Failed to add base package to environment")
|
|
302
|
+
|
|
303
|
+
# Verify base_pkg is in the environment
|
|
304
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
305
|
+
packages = env_data.get("packages", [])
|
|
306
|
+
self.assertEqual(len(packages), 1, "Base package not added correctly")
|
|
307
|
+
self.assertEqual(packages[0]["name"], "base_pkg", "Wrong package added")
|
|
308
|
+
|
|
309
|
+
# Now add complex_dep_pkg which depends on base_pkg, utility_pkg
|
|
310
|
+
# base_pkg should be satisfied, utility_pkg should need installation
|
|
311
|
+
complex_pkg_path = test_loader.packages_dir / "dependencies" / "complex_dep_pkg"
|
|
312
|
+
self.assertTrue(complex_pkg_path.exists(), f"Complex package not found: {complex_pkg_path}")
|
|
313
|
+
|
|
314
|
+
result = self.env_manager.add_package_to_environment(
|
|
315
|
+
str(complex_pkg_path),
|
|
316
|
+
"test_env",
|
|
317
|
+
auto_approve=True # Auto-approve for testing
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
self.assertTrue(result, "Failed to add package with mixed dependency states")
|
|
321
|
+
|
|
322
|
+
# Verify all required packages are now in the environment
|
|
323
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
324
|
+
packages = env_data.get("packages", [])
|
|
325
|
+
|
|
326
|
+
# Should have base_pkg (already present), utility_pkg, and complex_dep_pkg
|
|
327
|
+
expected_packages = ["base_pkg", "utility_pkg", "complex_dep_pkg"]
|
|
328
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
329
|
+
|
|
330
|
+
for pkg_name in expected_packages:
|
|
331
|
+
self.assertIn(pkg_name, package_names, f"Package {pkg_name} missing from environment")
|
|
332
|
+
|
|
333
|
+
@regression_test
|
|
334
|
+
@slow_test
|
|
335
|
+
def test_add_package_with_all_dependencies_already_present(self):
|
|
336
|
+
"""Test adding a package where all dependencies are already present."""
|
|
337
|
+
# Create an environment
|
|
338
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
339
|
+
self.env_manager.set_current_environment("test_env")
|
|
340
|
+
# First add all dependencies that simple_dep_pkg needs
|
|
341
|
+
from test_data_utils import TestDataLoader
|
|
342
|
+
test_loader = TestDataLoader()
|
|
343
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
344
|
+
self.assertTrue(base_pkg_path.exists(), f"Base package not found: {base_pkg_path}")
|
|
345
|
+
|
|
346
|
+
result = self.env_manager.add_package_to_environment(
|
|
347
|
+
str(base_pkg_path),
|
|
348
|
+
"test_env",
|
|
349
|
+
auto_approve=True # Auto-approve for testing
|
|
350
|
+
)
|
|
351
|
+
self.assertTrue(result, "Failed to add base package to environment")
|
|
352
|
+
|
|
353
|
+
# Verify base package is installed
|
|
354
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
355
|
+
packages = env_data.get("packages", [])
|
|
356
|
+
self.assertEqual(len(packages), 1, "Base package not added correctly")
|
|
357
|
+
|
|
358
|
+
# Now add simple_dep_pkg which only depends on base_pkg (which is already present)
|
|
359
|
+
simple_pkg_path = test_loader.packages_dir / "dependencies" / "simple_dep_pkg"
|
|
360
|
+
self.assertTrue(simple_pkg_path.exists(), f"Simple package not found: {simple_pkg_path}")
|
|
361
|
+
|
|
362
|
+
result = self.env_manager.add_package_to_environment(
|
|
363
|
+
str(simple_pkg_path),
|
|
364
|
+
"test_env",
|
|
365
|
+
auto_approve=True # Auto-approve for testing
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
self.assertTrue(result, "Failed to add package with all dependencies satisfied")
|
|
369
|
+
|
|
370
|
+
# Verify both packages are in the environment - no new dependencies should be added
|
|
371
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
372
|
+
packages = env_data.get("packages", [])
|
|
373
|
+
|
|
374
|
+
# Should have base_pkg (already present) and simple_dep_pkg (newly added)
|
|
375
|
+
expected_packages = ["base_pkg", "simple_dep_pkg"]
|
|
376
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
377
|
+
|
|
378
|
+
self.assertEqual(len(packages), 2, "Unexpected number of packages in environment")
|
|
379
|
+
for pkg_name in expected_packages:
|
|
380
|
+
self.assertIn(pkg_name, package_names, f"Package {pkg_name} missing from environment")
|
|
381
|
+
|
|
382
|
+
@regression_test
|
|
383
|
+
@slow_test
|
|
384
|
+
def test_add_package_with_version_constraint_satisfaction(self):
|
|
385
|
+
"""Test adding a package with version constraints where dependencies are satisfied."""
|
|
386
|
+
# Create an environment
|
|
387
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
388
|
+
self.env_manager.set_current_environment("test_env")
|
|
389
|
+
|
|
390
|
+
# Add base_pkg with a specific version
|
|
391
|
+
from test_data_utils import TestDataLoader
|
|
392
|
+
test_loader = TestDataLoader()
|
|
393
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
394
|
+
self.assertTrue(base_pkg_path.exists(), f"Base package not found: {base_pkg_path}")
|
|
395
|
+
|
|
396
|
+
result = self.env_manager.add_package_to_environment(
|
|
397
|
+
str(base_pkg_path),
|
|
398
|
+
"test_env",
|
|
399
|
+
auto_approve=True # Auto-approve for testing
|
|
400
|
+
)
|
|
401
|
+
self.assertTrue(result, "Failed to add base package to environment")
|
|
402
|
+
|
|
403
|
+
# Look for a package that has version constraints to test against
|
|
404
|
+
# For now, we'll simulate this by trying to add another package that depends on base_pkg
|
|
405
|
+
simple_pkg_path = test_loader.packages_dir / "dependencies" / "simple_dep_pkg"
|
|
406
|
+
self.assertTrue(simple_pkg_path.exists(), f"Simple package not found: {simple_pkg_path}")
|
|
407
|
+
|
|
408
|
+
result = self.env_manager.add_package_to_environment(
|
|
409
|
+
str(simple_pkg_path),
|
|
410
|
+
"test_env",
|
|
411
|
+
auto_approve=True # Auto-approve for testing
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
self.assertTrue(result, "Failed to add package with version constraint dependencies")
|
|
415
|
+
|
|
416
|
+
# Verify packages are correctly installed
|
|
417
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
418
|
+
packages = env_data.get("packages", [])
|
|
419
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
420
|
+
|
|
421
|
+
self.assertIn("base_pkg", package_names, "Base package missing from environment")
|
|
422
|
+
self.assertIn("simple_dep_pkg", package_names, "Dependent package missing from environment")
|
|
423
|
+
|
|
424
|
+
@integration_test(scope="component")
|
|
425
|
+
@slow_test
|
|
426
|
+
def test_add_package_with_mixed_dependency_types(self):
|
|
427
|
+
"""Test adding a package with mixed hatch and python dependencies."""
|
|
428
|
+
# Create an environment
|
|
429
|
+
self.env_manager.create_environment("test_env", "Test environment")
|
|
430
|
+
self.env_manager.set_current_environment("test_env")
|
|
431
|
+
|
|
432
|
+
# Add a package that has both hatch and python dependencies
|
|
433
|
+
from test_data_utils import TestDataLoader
|
|
434
|
+
test_loader = TestDataLoader()
|
|
435
|
+
python_dep_pkg_path = test_loader.packages_dir / "dependencies" / "python_dep_pkg"
|
|
436
|
+
self.assertTrue(python_dep_pkg_path.exists(), f"Python dependency package not found: {python_dep_pkg_path}")
|
|
437
|
+
|
|
438
|
+
result = self.env_manager.add_package_to_environment(
|
|
439
|
+
str(python_dep_pkg_path),
|
|
440
|
+
"test_env",
|
|
441
|
+
auto_approve=True # Auto-approve for testing
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
self.assertTrue(result, "Failed to add package with mixed dependency types")
|
|
445
|
+
|
|
446
|
+
# Verify package was added
|
|
447
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
448
|
+
packages = env_data.get("packages", [])
|
|
449
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
450
|
+
|
|
451
|
+
self.assertIn("python_dep_pkg", package_names, "Package with mixed dependencies missing from environment")
|
|
452
|
+
|
|
453
|
+
# Now add a package that depends on the python_dep_pkg (should be satisfied)
|
|
454
|
+
# and also depends on other packages (should need installation)
|
|
455
|
+
complex_pkg_path = test_loader.packages_dir / "dependencies" / "complex_dep_pkg"
|
|
456
|
+
self.assertTrue(complex_pkg_path.exists(), f"Complex package not found: {complex_pkg_path}")
|
|
457
|
+
|
|
458
|
+
result = self.env_manager.add_package_to_environment(
|
|
459
|
+
str(complex_pkg_path),
|
|
460
|
+
"test_env",
|
|
461
|
+
auto_approve=True # Auto-approve for testing
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
self.assertTrue(result, "Failed to add package with mixed satisfied/unsatisfied dependencies")
|
|
465
|
+
|
|
466
|
+
# Verify all expected packages are present
|
|
467
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
468
|
+
packages = env_data.get("packages", [])
|
|
469
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
470
|
+
|
|
471
|
+
# Should have python_dep_pkg (already present) plus any other dependencies of complex_dep_pkg
|
|
472
|
+
self.assertIn("python_dep_pkg", package_names, "Originally installed package missing")
|
|
473
|
+
self.assertIn("complex_dep_pkg", package_names, "New package missing from environment")
|
|
474
|
+
|
|
475
|
+
# Python dep package has a dep to request. This should be satisfied in the python environment
|
|
476
|
+
python_env_info = self.env_manager.python_env_manager.get_environment_info("test_env")
|
|
477
|
+
packages = python_env_info.get("packages", [])
|
|
478
|
+
self.assertIsNotNone(packages, "Python environment packages not found")
|
|
479
|
+
self.assertGreater(len(packages), 0, "No packages found in Python environment")
|
|
480
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
481
|
+
self.assertIn("requests", package_names, f"Expected 'requests' package not found in Python environment: {packages}")
|
|
482
|
+
|
|
483
|
+
@integration_test(scope="system")
|
|
484
|
+
@slow_test
|
|
485
|
+
@unittest.skipIf(sys.platform.startswith("win"), "System dependency test skipped on Windows")
|
|
486
|
+
def test_add_package_with_system_dependency(self):
|
|
487
|
+
"""Test adding a package with a system dependency."""
|
|
488
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
489
|
+
self.env_manager.set_current_environment("test_env")
|
|
490
|
+
# Add a package that declares a system dependency (e.g., 'curl')
|
|
491
|
+
system_dep_pkg_path = self.hatch_dev_path / "system_dep_pkg"
|
|
492
|
+
self.assertTrue(system_dep_pkg_path.exists(), f"System dependency package not found: {system_dep_pkg_path}")
|
|
493
|
+
|
|
494
|
+
result = self.env_manager.add_package_to_environment(
|
|
495
|
+
str(system_dep_pkg_path),
|
|
496
|
+
"test_env",
|
|
497
|
+
auto_approve=True
|
|
498
|
+
)
|
|
499
|
+
self.assertTrue(result, "Failed to add package with system dependency")
|
|
500
|
+
|
|
501
|
+
# Verify package was added
|
|
502
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
503
|
+
packages = env_data.get("packages", [])
|
|
504
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
505
|
+
self.assertIn("system_dep_pkg", package_names, "System dependency package missing from environment")
|
|
506
|
+
|
|
507
|
+
# Skip if Docker is not available
|
|
508
|
+
@integration_test(scope="service")
|
|
509
|
+
@slow_test
|
|
510
|
+
@unittest.skipUnless(DOCKER_DAEMON_AVAILABLE, "Docker dependency test skipped due to Docker not being available")
|
|
511
|
+
def test_add_package_with_docker_dependency(self):
|
|
512
|
+
"""Test adding a package with a docker dependency."""
|
|
513
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
514
|
+
self.env_manager.set_current_environment("test_env")
|
|
515
|
+
# Add a package that declares a docker dependency (e.g., 'redis:latest')
|
|
516
|
+
docker_dep_pkg_path = self.hatch_dev_path / "docker_dep_pkg"
|
|
517
|
+
self.assertTrue(docker_dep_pkg_path.exists(), f"Docker dependency package not found: {docker_dep_pkg_path}")
|
|
518
|
+
|
|
519
|
+
result = self.env_manager.add_package_to_environment(
|
|
520
|
+
str(docker_dep_pkg_path),
|
|
521
|
+
"test_env",
|
|
522
|
+
auto_approve=True
|
|
523
|
+
)
|
|
524
|
+
self.assertTrue(result, "Failed to add package with docker dependency")
|
|
525
|
+
|
|
526
|
+
# Verify package was added
|
|
527
|
+
env_data = self.env_manager.get_environments().get("test_env")
|
|
528
|
+
packages = env_data.get("packages", [])
|
|
529
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
530
|
+
self.assertIn("docker_dep_pkg", package_names, "Docker dependency package missing from environment")
|
|
531
|
+
|
|
532
|
+
@regression_test
|
|
533
|
+
@slow_test
|
|
534
|
+
def test_create_environment_with_mcp_server_default(self):
|
|
535
|
+
"""Test creating environment with default MCP server installation."""
|
|
536
|
+
# Mock the MCP server installation to avoid actual network calls
|
|
537
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
538
|
+
installed_env = None
|
|
539
|
+
installed_tag = None
|
|
540
|
+
|
|
541
|
+
def mock_install(env_name, tag=None):
|
|
542
|
+
nonlocal installed_env, installed_tag
|
|
543
|
+
installed_env = env_name
|
|
544
|
+
installed_tag = tag
|
|
545
|
+
# Simulate successful installation
|
|
546
|
+
package_git_url = "git+https://github.com/CrackingShells/Hatch-MCP-Server.git"
|
|
547
|
+
env_data = self.env_manager._environments[env_name]
|
|
548
|
+
env_data["packages"].append({
|
|
549
|
+
"name": f"hatch_mcp_server @ {package_git_url}",
|
|
550
|
+
"version": "dev",
|
|
551
|
+
"type": "python",
|
|
552
|
+
"source": package_git_url,
|
|
553
|
+
"installed_at": datetime.now().isoformat()
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# Create environment without Python environment but simulate that it has one
|
|
560
|
+
success = self.env_manager.create_environment("test_mcp_default",
|
|
561
|
+
description="Test MCP default",
|
|
562
|
+
create_python_env=False, # Don't create actual Python env
|
|
563
|
+
no_hatch_mcp_server=False)
|
|
564
|
+
|
|
565
|
+
# Manually set python_env info to simulate having Python support
|
|
566
|
+
self.env_manager._environments["test_mcp_default"]["python_env"] = {
|
|
567
|
+
"enabled": True,
|
|
568
|
+
"conda_env_name": "hatch-test_mcp_default",
|
|
569
|
+
"python_executable": "/fake/python",
|
|
570
|
+
"created_at": datetime.now().isoformat(),
|
|
571
|
+
"version": "3.11.0",
|
|
572
|
+
"manager": "conda"
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
# Now call the MCP installation manually (since we bypassed Python env creation)
|
|
576
|
+
self.env_manager._install_hatch_mcp_server("test_mcp_default", None)
|
|
577
|
+
|
|
578
|
+
self.assertTrue(success, "Environment creation should succeed")
|
|
579
|
+
self.assertEqual(installed_env, "test_mcp_default", "MCP server should be installed in correct environment")
|
|
580
|
+
self.assertIsNone(installed_tag, "Default installation should use no specific tag")
|
|
581
|
+
|
|
582
|
+
# Verify MCP server package is in environment
|
|
583
|
+
env_data = self.env_manager._environments["test_mcp_default"]
|
|
584
|
+
packages = env_data.get("packages", [])
|
|
585
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
586
|
+
expected_name = "hatch_mcp_server @ git+https://github.com/CrackingShells/Hatch-MCP-Server.git"
|
|
587
|
+
self.assertIn(expected_name, package_names, "MCP server should be installed by default with correct name syntax")
|
|
588
|
+
|
|
589
|
+
finally:
|
|
590
|
+
# Restore original method
|
|
591
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
592
|
+
|
|
593
|
+
@regression_test
|
|
594
|
+
@slow_test
|
|
595
|
+
def test_create_environment_with_mcp_server_opt_out(self):
|
|
596
|
+
"""Test creating environment with MCP server installation opted out."""
|
|
597
|
+
# Mock the MCP server installation to track calls
|
|
598
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
599
|
+
install_called = False
|
|
600
|
+
|
|
601
|
+
def mock_install(env_name, tag=None):
|
|
602
|
+
nonlocal install_called
|
|
603
|
+
install_called = True
|
|
604
|
+
|
|
605
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
606
|
+
|
|
607
|
+
try:
|
|
608
|
+
# Create environment without Python environment, MCP server opted out
|
|
609
|
+
success = self.env_manager.create_environment("test_mcp_opt_out",
|
|
610
|
+
description="Test MCP opt out",
|
|
611
|
+
create_python_env=False, # Don't create actual Python env
|
|
612
|
+
no_hatch_mcp_server=True)
|
|
613
|
+
|
|
614
|
+
# Manually set python_env info to simulate having Python support
|
|
615
|
+
self.env_manager._environments["test_mcp_opt_out"]["python_env"] = {
|
|
616
|
+
"enabled": True,
|
|
617
|
+
"conda_env_name": "hatch-test_mcp_opt_out",
|
|
618
|
+
"python_executable": "/fake/python",
|
|
619
|
+
"created_at": datetime.now().isoformat(),
|
|
620
|
+
"version": "3.11.0",
|
|
621
|
+
"manager": "conda"
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
self.assertTrue(success, "Environment creation should succeed")
|
|
625
|
+
self.assertFalse(install_called, "MCP server installation should not be called when opted out")
|
|
626
|
+
|
|
627
|
+
# Verify MCP server package is NOT in environment
|
|
628
|
+
env_data = self.env_manager._environments["test_mcp_opt_out"]
|
|
629
|
+
packages = env_data.get("packages", [])
|
|
630
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
631
|
+
expected_name = "hatch_mcp_server @ git+https://github.com/CrackingShells/Hatch-MCP-Server.git"
|
|
632
|
+
self.assertNotIn(expected_name, package_names, "MCP server should not be installed when opted out")
|
|
633
|
+
|
|
634
|
+
finally:
|
|
635
|
+
# Restore original method
|
|
636
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
637
|
+
|
|
638
|
+
@regression_test
|
|
639
|
+
@slow_test
|
|
640
|
+
def test_create_environment_with_mcp_server_custom_tag(self):
|
|
641
|
+
"""Test creating environment with custom MCP server tag."""
|
|
642
|
+
# Mock the MCP server installation to avoid actual network calls
|
|
643
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
644
|
+
installed_tag = None
|
|
645
|
+
|
|
646
|
+
def mock_install(env_name, tag=None):
|
|
647
|
+
nonlocal installed_tag
|
|
648
|
+
installed_tag = tag
|
|
649
|
+
# Simulate successful installation
|
|
650
|
+
package_git_url = f"git+https://github.com/CrackingShells/Hatch-MCP-Server.git@{tag}"
|
|
651
|
+
env_data = self.env_manager._environments[env_name]
|
|
652
|
+
env_data["packages"].append({
|
|
653
|
+
"name": f"hatch_mcp_server @ {package_git_url}",
|
|
654
|
+
"version": tag or "latest",
|
|
655
|
+
"type": "python",
|
|
656
|
+
"source": package_git_url,
|
|
657
|
+
"installed_at": datetime.now().isoformat()
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
661
|
+
|
|
662
|
+
try:
|
|
663
|
+
# Create environment without Python environment
|
|
664
|
+
success = self.env_manager.create_environment("test_mcp_custom_tag",
|
|
665
|
+
description="Test MCP custom tag",
|
|
666
|
+
create_python_env=False, # Don't create actual Python env
|
|
667
|
+
no_hatch_mcp_server=False,
|
|
668
|
+
hatch_mcp_server_tag="v0.1.0")
|
|
669
|
+
|
|
670
|
+
# Manually set python_env info to simulate having Python support
|
|
671
|
+
self.env_manager._environments["test_mcp_custom_tag"]["python_env"] = {
|
|
672
|
+
"enabled": True,
|
|
673
|
+
"conda_env_name": "hatch-test_mcp_custom_tag",
|
|
674
|
+
"python_executable": "/fake/python",
|
|
675
|
+
"created_at": datetime.now().isoformat(),
|
|
676
|
+
"version": "3.11.0",
|
|
677
|
+
"manager": "conda"
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# Now call the MCP installation manually (since we bypassed Python env creation)
|
|
681
|
+
self.env_manager._install_hatch_mcp_server("test_mcp_custom_tag", "v0.1.0")
|
|
682
|
+
|
|
683
|
+
self.assertTrue(success, "Environment creation should succeed")
|
|
684
|
+
self.assertEqual(installed_tag, "v0.1.0", "Custom tag should be passed to installation")
|
|
685
|
+
|
|
686
|
+
# Verify MCP server package is in environment with correct version
|
|
687
|
+
env_data = self.env_manager._environments["test_mcp_custom_tag"]
|
|
688
|
+
packages = env_data.get("packages", [])
|
|
689
|
+
expected_name = "hatch_mcp_server @ git+https://github.com/CrackingShells/Hatch-MCP-Server.git@v0.1.0"
|
|
690
|
+
mcp_packages = [pkg for pkg in packages if pkg["name"] == expected_name]
|
|
691
|
+
self.assertEqual(len(mcp_packages), 1, "Exactly one MCP server package should be installed with correct name syntax")
|
|
692
|
+
self.assertEqual(mcp_packages[0]["version"], "v0.1.0", "MCP server should have correct version")
|
|
693
|
+
|
|
694
|
+
finally:
|
|
695
|
+
# Restore original method
|
|
696
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
697
|
+
|
|
698
|
+
@regression_test
|
|
699
|
+
@slow_test
|
|
700
|
+
def test_create_environment_no_python_no_mcp_server(self):
|
|
701
|
+
"""Test creating environment without Python support should not install MCP server."""
|
|
702
|
+
# Mock the MCP server installation to track calls
|
|
703
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
704
|
+
install_called = False
|
|
705
|
+
|
|
706
|
+
def mock_install(env_name, tag=None):
|
|
707
|
+
nonlocal install_called
|
|
708
|
+
install_called = True
|
|
709
|
+
|
|
710
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
711
|
+
|
|
712
|
+
try:
|
|
713
|
+
# Create environment without Python support
|
|
714
|
+
success = self.env_manager.create_environment("test_no_python",
|
|
715
|
+
description="Test no Python",
|
|
716
|
+
create_python_env=False,
|
|
717
|
+
no_hatch_mcp_server=False)
|
|
718
|
+
|
|
719
|
+
self.assertTrue(success, "Environment creation should succeed")
|
|
720
|
+
self.assertFalse(install_called, "MCP server installation should not be called without Python environment")
|
|
721
|
+
|
|
722
|
+
finally:
|
|
723
|
+
# Restore original method
|
|
724
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
725
|
+
|
|
726
|
+
@regression_test
|
|
727
|
+
@slow_test
|
|
728
|
+
def test_install_mcp_server_existing_environment(self):
|
|
729
|
+
"""Test installing MCP server in an existing environment."""
|
|
730
|
+
# Create environment first without Python environment
|
|
731
|
+
success = self.env_manager.create_environment("test_existing_mcp",
|
|
732
|
+
description="Test existing MCP",
|
|
733
|
+
create_python_env=False, # Don't create actual Python env
|
|
734
|
+
no_hatch_mcp_server=True) # Opt out initially
|
|
735
|
+
self.assertTrue(success, "Environment creation should succeed")
|
|
736
|
+
|
|
737
|
+
# Manually set python_env info to simulate having Python support
|
|
738
|
+
self.env_manager._environments["test_existing_mcp"]["python_env"] = {
|
|
739
|
+
"enabled": True,
|
|
740
|
+
"conda_env_name": "hatch-test_existing_mcp",
|
|
741
|
+
"python_executable": "/fake/python",
|
|
742
|
+
"created_at": datetime.now().isoformat(),
|
|
743
|
+
"version": "3.11.0",
|
|
744
|
+
"manager": "conda"
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
# Mock the MCP server installation
|
|
748
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
749
|
+
installed_env = None
|
|
750
|
+
installed_tag = None
|
|
751
|
+
|
|
752
|
+
def mock_install(env_name, tag=None):
|
|
753
|
+
nonlocal installed_env, installed_tag
|
|
754
|
+
installed_env = env_name
|
|
755
|
+
installed_tag = tag
|
|
756
|
+
# Simulate successful installation
|
|
757
|
+
package_git_url = f"git+https://github.com/CrackingShells/Hatch-MCP-Server.git@{tag if tag else 'main'}"
|
|
758
|
+
env_data = self.env_manager._environments[env_name]
|
|
759
|
+
env_data["packages"].append({
|
|
760
|
+
"name": f"hatch_mcp_server @ {package_git_url}",
|
|
761
|
+
"version": tag or "latest",
|
|
762
|
+
"type": "python",
|
|
763
|
+
"source": package_git_url,
|
|
764
|
+
"installed_at": datetime.now().isoformat()
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
# Install MCP server with custom tag
|
|
771
|
+
success = self.env_manager.install_mcp_server("test_existing_mcp", "v0.2.0")
|
|
772
|
+
|
|
773
|
+
self.assertTrue(success, "MCP server installation should succeed")
|
|
774
|
+
self.assertEqual(installed_env, "test_existing_mcp", "MCP server should be installed in correct environment")
|
|
775
|
+
self.assertEqual(installed_tag, "v0.2.0", "Custom tag should be passed to installation")
|
|
776
|
+
|
|
777
|
+
# Verify MCP server package is in environment
|
|
778
|
+
env_data = self.env_manager._environments["test_existing_mcp"]
|
|
779
|
+
packages = env_data.get("packages", [])
|
|
780
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
781
|
+
expected_name = f"hatch_mcp_server @ git+https://github.com/CrackingShells/Hatch-MCP-Server.git@v0.2.0"
|
|
782
|
+
self.assertIn(expected_name, package_names, "MCP server should be installed in environment with correct name syntax")
|
|
783
|
+
|
|
784
|
+
finally:
|
|
785
|
+
# Restore original method
|
|
786
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
787
|
+
|
|
788
|
+
@regression_test
|
|
789
|
+
@slow_test
|
|
790
|
+
def test_create_python_environment_only_with_mcp_wrapper(self):
|
|
791
|
+
"""Test creating Python environment only with MCP wrapper support."""
|
|
792
|
+
# First create a Hatch environment without Python
|
|
793
|
+
self.env_manager.create_environment("test_python_only", "Test Python Only", create_python_env=False)
|
|
794
|
+
self.assertTrue(self.env_manager.environment_exists("test_python_only"))
|
|
795
|
+
|
|
796
|
+
# Mock Python environment creation to simulate success
|
|
797
|
+
original_create = self.env_manager.python_env_manager.create_python_environment
|
|
798
|
+
original_get_info = self.env_manager.python_env_manager.get_environment_info
|
|
799
|
+
|
|
800
|
+
def mock_create_python_env(env_name, python_version=None, force=False):
|
|
801
|
+
return True
|
|
802
|
+
|
|
803
|
+
def mock_get_env_info(env_name):
|
|
804
|
+
return {
|
|
805
|
+
"conda_env_name": f"hatch-{env_name}",
|
|
806
|
+
"python_executable": f"/path/to/conda/envs/hatch-{env_name}/bin/python",
|
|
807
|
+
"python_version": "3.11.0",
|
|
808
|
+
"manager": "conda"
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
# Mock MCP wrapper installation
|
|
812
|
+
installed_env = None
|
|
813
|
+
installed_tag = None
|
|
814
|
+
original_install = self.env_manager._install_hatch_mcp_server
|
|
815
|
+
|
|
816
|
+
def mock_install(env_name, tag=None):
|
|
817
|
+
nonlocal installed_env, installed_tag
|
|
818
|
+
installed_env = env_name
|
|
819
|
+
installed_tag = tag
|
|
820
|
+
# Simulate adding MCP wrapper to environment
|
|
821
|
+
package_git_url = f"git+https://github.com/CrackingShells/Hatch-MCP-Server.git"
|
|
822
|
+
if tag:
|
|
823
|
+
package_git_url += f"@{tag}"
|
|
824
|
+
env_data = self.env_manager._environments[env_name]
|
|
825
|
+
env_data["packages"].append({
|
|
826
|
+
"name": f"hatch_mcp_server @ {package_git_url}",
|
|
827
|
+
"version": tag or "latest",
|
|
828
|
+
"type": "python",
|
|
829
|
+
"source": package_git_url,
|
|
830
|
+
"installed_at": datetime.now().isoformat()
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
self.env_manager.python_env_manager.create_python_environment = mock_create_python_env
|
|
834
|
+
self.env_manager.python_env_manager.get_environment_info = mock_get_env_info
|
|
835
|
+
self.env_manager._install_hatch_mcp_server = mock_install
|
|
836
|
+
|
|
837
|
+
try:
|
|
838
|
+
# Test creating Python environment with default MCP wrapper installation
|
|
839
|
+
success = self.env_manager.create_python_environment_only("test_python_only")
|
|
840
|
+
|
|
841
|
+
self.assertTrue(success, "Python environment creation should succeed")
|
|
842
|
+
self.assertEqual(installed_env, "test_python_only", "MCP wrapper should be installed in correct environment")
|
|
843
|
+
self.assertIsNone(installed_tag, "Default tag should be None")
|
|
844
|
+
|
|
845
|
+
# Verify environment metadata was updated
|
|
846
|
+
env_data = self.env_manager._environments["test_python_only"]
|
|
847
|
+
self.assertTrue(env_data.get("python_environment"), "Python environment flag should be set")
|
|
848
|
+
self.assertIsNotNone(env_data.get("python_env"), "Python environment info should be set")
|
|
849
|
+
|
|
850
|
+
# Verify MCP wrapper was installed
|
|
851
|
+
packages = env_data.get("packages", [])
|
|
852
|
+
package_names = [pkg["name"] for pkg in packages]
|
|
853
|
+
expected_name = "hatch_mcp_server @ git+https://github.com/CrackingShells/Hatch-MCP-Server.git"
|
|
854
|
+
self.assertIn(expected_name, package_names, "MCP wrapper should be installed")
|
|
855
|
+
|
|
856
|
+
# Reset for next test
|
|
857
|
+
installed_env = None
|
|
858
|
+
installed_tag = None
|
|
859
|
+
env_data["packages"] = []
|
|
860
|
+
|
|
861
|
+
# Test creating Python environment with custom tag
|
|
862
|
+
success = self.env_manager.create_python_environment_only(
|
|
863
|
+
"test_python_only",
|
|
864
|
+
python_version="3.12",
|
|
865
|
+
force=True,
|
|
866
|
+
hatch_mcp_server_tag="dev"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
self.assertTrue(success, "Python environment creation with custom tag should succeed")
|
|
870
|
+
self.assertEqual(installed_tag, "dev", "Custom tag should be passed to MCP wrapper installation")
|
|
871
|
+
|
|
872
|
+
# Reset for next test
|
|
873
|
+
installed_env = None
|
|
874
|
+
env_data["packages"] = []
|
|
875
|
+
|
|
876
|
+
# Test opting out of MCP wrapper installation
|
|
877
|
+
success = self.env_manager.create_python_environment_only(
|
|
878
|
+
"test_python_only",
|
|
879
|
+
force=True,
|
|
880
|
+
no_hatch_mcp_server=True
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
self.assertTrue(success, "Python environment creation without MCP wrapper should succeed")
|
|
884
|
+
self.assertIsNone(installed_env, "MCP wrapper should not be installed when opted out")
|
|
885
|
+
|
|
886
|
+
# Verify no MCP wrapper was installed
|
|
887
|
+
packages = env_data.get("packages", [])
|
|
888
|
+
self.assertEqual(len(packages), 0, "No packages should be installed when MCP wrapper is opted out")
|
|
889
|
+
|
|
890
|
+
finally:
|
|
891
|
+
# Restore original methods
|
|
892
|
+
self.env_manager.python_env_manager.create_python_environment = original_create
|
|
893
|
+
self.env_manager.python_env_manager.get_environment_info = original_get_info
|
|
894
|
+
self.env_manager._install_hatch_mcp_server = original_install
|
|
895
|
+
|
|
896
|
+
# Non-TTY Handling Backward Compatibility Tests
|
|
897
|
+
|
|
898
|
+
@regression_test
|
|
899
|
+
@patch('sys.stdin.isatty', return_value=False)
|
|
900
|
+
def test_add_package_non_tty_auto_approve(self, mock_isatty):
|
|
901
|
+
"""Test package addition in non-TTY environment (backward compatibility)."""
|
|
902
|
+
# Create environment
|
|
903
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
904
|
+
|
|
905
|
+
# Test existing auto_approve=True behavior is preserved
|
|
906
|
+
from test_data_utils import TestDataLoader
|
|
907
|
+
test_loader = TestDataLoader()
|
|
908
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
909
|
+
|
|
910
|
+
if not base_pkg_path.exists():
|
|
911
|
+
self.skipTest(f"Test package not found: {base_pkg_path}")
|
|
912
|
+
|
|
913
|
+
result = self.env_manager.add_package_to_environment(
|
|
914
|
+
str(base_pkg_path),
|
|
915
|
+
"test_env",
|
|
916
|
+
auto_approve=False # Should auto-approve due to non-TTY detection
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
self.assertTrue(result, "Non-TTY environment should auto-approve even with auto_approve=False")
|
|
920
|
+
mock_isatty.assert_called() # Verify TTY detection was called
|
|
921
|
+
|
|
922
|
+
@regression_test
|
|
923
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': '1'})
|
|
924
|
+
def test_add_package_environment_variable_compatibility(self):
|
|
925
|
+
"""Test new environment variable doesn't break existing workflows."""
|
|
926
|
+
# Verify existing auto_approve=False behavior with environment variable
|
|
927
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
928
|
+
|
|
929
|
+
from test_data_utils import TestDataLoader
|
|
930
|
+
test_loader = TestDataLoader()
|
|
931
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
932
|
+
|
|
933
|
+
if not base_pkg_path.exists():
|
|
934
|
+
self.skipTest(f"Test package not found: {base_pkg_path}")
|
|
935
|
+
|
|
936
|
+
result = self.env_manager.add_package_to_environment(
|
|
937
|
+
str(base_pkg_path),
|
|
938
|
+
"test_env",
|
|
939
|
+
auto_approve=False # Should be overridden by environment variable
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
self.assertTrue(result, "Environment variable should enable auto-approval")
|
|
943
|
+
|
|
944
|
+
@regression_test
|
|
945
|
+
@patch('sys.stdin.isatty', return_value=False)
|
|
946
|
+
def test_add_package_with_dependencies_non_tty(self, mock_isatty):
|
|
947
|
+
"""Test package with dependencies in non-TTY environment."""
|
|
948
|
+
# Create environment
|
|
949
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
950
|
+
|
|
951
|
+
from test_data_utils import TestDataLoader
|
|
952
|
+
test_loader = TestDataLoader()
|
|
953
|
+
|
|
954
|
+
# Test with a package that has dependencies
|
|
955
|
+
simple_pkg_path = test_loader.packages_dir / "dependencies" / "simple_dep_pkg"
|
|
956
|
+
|
|
957
|
+
if not simple_pkg_path.exists():
|
|
958
|
+
self.skipTest(f"Test package not found: {simple_pkg_path}")
|
|
959
|
+
|
|
960
|
+
result = self.env_manager.add_package_to_environment(
|
|
961
|
+
str(simple_pkg_path),
|
|
962
|
+
"test_env",
|
|
963
|
+
auto_approve=False # Should auto-approve due to non-TTY
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
self.assertTrue(result, "Package with dependencies should install in non-TTY")
|
|
967
|
+
mock_isatty.assert_called()
|
|
968
|
+
|
|
969
|
+
@regression_test
|
|
970
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'yes'})
|
|
971
|
+
def test_environment_variable_case_variations(self):
|
|
972
|
+
"""Test environment variable with different case variations."""
|
|
973
|
+
self.env_manager.create_environment("test_env", "Test environment", create_python_env=False)
|
|
974
|
+
|
|
975
|
+
from test_data_utils import TestDataLoader
|
|
976
|
+
test_loader = TestDataLoader()
|
|
977
|
+
base_pkg_path = test_loader.packages_dir / "basic" / "base_pkg"
|
|
978
|
+
|
|
979
|
+
if not base_pkg_path.exists():
|
|
980
|
+
self.skipTest(f"Test package not found: {base_pkg_path}")
|
|
981
|
+
|
|
982
|
+
result = self.env_manager.add_package_to_environment(
|
|
983
|
+
str(base_pkg_path),
|
|
984
|
+
"test_env",
|
|
985
|
+
auto_approve=False
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
self.assertTrue(result, "Environment variable 'yes' should enable auto-approval")
|
|
989
|
+
|
|
990
|
+
if __name__ == "__main__":
|
|
991
|
+
unittest.main()
|