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,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()