hatch-xclam 0.7.1.dev3__py3-none-any.whl → 0.8.0.dev1__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 (81) hide show
  1. hatch/__init__.py +1 -1
  2. hatch/cli/__init__.py +71 -0
  3. hatch/cli/__main__.py +1035 -0
  4. hatch/cli/cli_env.py +865 -0
  5. hatch/cli/cli_mcp.py +1965 -0
  6. hatch/cli/cli_package.py +566 -0
  7. hatch/cli/cli_system.py +136 -0
  8. hatch/cli/cli_utils.py +1289 -0
  9. hatch/cli_hatch.py +160 -2838
  10. hatch/mcp_host_config/__init__.py +10 -10
  11. hatch/mcp_host_config/adapters/__init__.py +34 -0
  12. hatch/mcp_host_config/adapters/base.py +170 -0
  13. hatch/mcp_host_config/adapters/claude.py +105 -0
  14. hatch/mcp_host_config/adapters/codex.py +104 -0
  15. hatch/mcp_host_config/adapters/cursor.py +83 -0
  16. hatch/mcp_host_config/adapters/gemini.py +75 -0
  17. hatch/mcp_host_config/adapters/kiro.py +78 -0
  18. hatch/mcp_host_config/adapters/lmstudio.py +79 -0
  19. hatch/mcp_host_config/adapters/registry.py +149 -0
  20. hatch/mcp_host_config/adapters/vscode.py +83 -0
  21. hatch/mcp_host_config/backup.py +5 -3
  22. hatch/mcp_host_config/fields.py +126 -0
  23. hatch/mcp_host_config/models.py +161 -456
  24. hatch/mcp_host_config/reporting.py +57 -16
  25. hatch/mcp_host_config/strategies.py +155 -87
  26. hatch/template_generator.py +1 -1
  27. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
  28. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
  29. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
  30. hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
  31. tests/cli_test_utils.py +280 -0
  32. tests/integration/cli/__init__.py +14 -0
  33. tests/integration/cli/test_cli_reporter_integration.py +2439 -0
  34. tests/integration/mcp/__init__.py +0 -0
  35. tests/integration/mcp/test_adapter_serialization.py +173 -0
  36. tests/regression/cli/__init__.py +16 -0
  37. tests/regression/cli/test_color_logic.py +268 -0
  38. tests/regression/cli/test_consequence_type.py +298 -0
  39. tests/regression/cli/test_error_formatting.py +328 -0
  40. tests/regression/cli/test_result_reporter.py +586 -0
  41. tests/regression/cli/test_table_formatter.py +211 -0
  42. tests/regression/mcp/__init__.py +0 -0
  43. tests/regression/mcp/test_field_filtering.py +162 -0
  44. tests/test_cli_version.py +7 -5
  45. tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
  46. tests/unit/__init__.py +0 -0
  47. tests/unit/mcp/__init__.py +0 -0
  48. tests/unit/mcp/test_adapter_protocol.py +138 -0
  49. tests/unit/mcp/test_adapter_registry.py +158 -0
  50. tests/unit/mcp/test_config_model.py +146 -0
  51. hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
  52. tests/integration/test_mcp_kiro_integration.py +0 -153
  53. tests/regression/test_mcp_codex_backup_integration.py +0 -162
  54. tests/regression/test_mcp_codex_host_strategy.py +0 -163
  55. tests/regression/test_mcp_codex_model_validation.py +0 -117
  56. tests/regression/test_mcp_kiro_backup_integration.py +0 -241
  57. tests/regression/test_mcp_kiro_cli_integration.py +0 -141
  58. tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
  59. tests/regression/test_mcp_kiro_host_strategy.py +0 -214
  60. tests/regression/test_mcp_kiro_model_validation.py +0 -116
  61. tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
  62. tests/test_mcp_atomic_operations.py +0 -276
  63. tests/test_mcp_backup_integration.py +0 -308
  64. tests/test_mcp_cli_all_host_specific_args.py +0 -496
  65. tests/test_mcp_cli_backup_management.py +0 -295
  66. tests/test_mcp_cli_direct_management.py +0 -456
  67. tests/test_mcp_cli_discovery_listing.py +0 -582
  68. tests/test_mcp_cli_host_config_integration.py +0 -823
  69. tests/test_mcp_cli_package_management.py +0 -360
  70. tests/test_mcp_cli_partial_updates.py +0 -859
  71. tests/test_mcp_environment_integration.py +0 -520
  72. tests/test_mcp_host_config_backup.py +0 -257
  73. tests/test_mcp_host_configuration_manager.py +0 -331
  74. tests/test_mcp_host_registry_decorator.py +0 -348
  75. tests/test_mcp_pydantic_architecture_v4.py +0 -603
  76. tests/test_mcp_server_config_models.py +0 -242
  77. tests/test_mcp_server_config_type_field.py +0 -221
  78. tests/test_mcp_sync_functionality.py +0 -316
  79. tests/test_mcp_user_feedback_reporting.py +0 -359
  80. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
  81. {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,173 @@
1
+ """Integration tests for adapter serialization.
2
+
3
+ Test IDs: AS-01 to AS-10 (per 02-test_architecture_rebuild_v0.md)
4
+ Scope: Full serialization flow for each adapter with realistic configs.
5
+ """
6
+
7
+ import unittest
8
+
9
+ from hatch.mcp_host_config.models import MCPServerConfig
10
+ from hatch.mcp_host_config.adapters import (
11
+ ClaudeAdapter,
12
+ CodexAdapter,
13
+ CursorAdapter,
14
+ GeminiAdapter,
15
+ KiroAdapter,
16
+ LMStudioAdapter,
17
+ VSCodeAdapter,
18
+ )
19
+
20
+
21
+ class TestClaudeAdapterSerialization(unittest.TestCase):
22
+ """Integration tests for Claude adapter serialization."""
23
+
24
+ def test_AS01_claude_stdio_serialization(self):
25
+ """AS-01: Claude stdio config serializes correctly."""
26
+ config = MCPServerConfig(
27
+ name="my-server",
28
+ command="python",
29
+ args=["-m", "mcp_server"],
30
+ env={"API_KEY": "secret"},
31
+ type="stdio",
32
+ )
33
+
34
+ adapter = ClaudeAdapter()
35
+ result = adapter.serialize(config)
36
+
37
+ self.assertEqual(result["command"], "python")
38
+ self.assertEqual(result["args"], ["-m", "mcp_server"])
39
+ self.assertEqual(result["env"], {"API_KEY": "secret"})
40
+ self.assertEqual(result["type"], "stdio")
41
+ self.assertNotIn("name", result)
42
+
43
+ def test_AS02_claude_sse_serialization(self):
44
+ """AS-02: Claude SSE config serializes correctly."""
45
+ config = MCPServerConfig(
46
+ name="remote-server",
47
+ url="https://api.example.com/mcp",
48
+ headers={"Authorization": "Bearer token"},
49
+ type="sse",
50
+ )
51
+
52
+ adapter = ClaudeAdapter()
53
+ result = adapter.serialize(config)
54
+
55
+ self.assertEqual(result["url"], "https://api.example.com/mcp")
56
+ self.assertEqual(result["headers"], {"Authorization": "Bearer token"})
57
+ self.assertEqual(result["type"], "sse")
58
+ self.assertNotIn("name", result)
59
+ self.assertNotIn("command", result)
60
+
61
+
62
+ class TestGeminiAdapterSerialization(unittest.TestCase):
63
+ """Integration tests for Gemini adapter serialization."""
64
+
65
+ def test_AS03_gemini_stdio_serialization(self):
66
+ """AS-03: Gemini stdio config serializes correctly."""
67
+ config = MCPServerConfig(
68
+ name="gemini-server",
69
+ command="npx",
70
+ args=["mcp-server"],
71
+ cwd="/workspace",
72
+ timeout=30000,
73
+ )
74
+
75
+ adapter = GeminiAdapter()
76
+ result = adapter.serialize(config)
77
+
78
+ self.assertEqual(result["command"], "npx")
79
+ self.assertEqual(result["args"], ["mcp-server"])
80
+ self.assertEqual(result["cwd"], "/workspace")
81
+ self.assertEqual(result["timeout"], 30000)
82
+ self.assertNotIn("name", result)
83
+ self.assertNotIn("type", result)
84
+
85
+ def test_AS04_gemini_http_serialization(self):
86
+ """AS-04: Gemini HTTP config serializes correctly."""
87
+ config = MCPServerConfig(
88
+ name="gemini-http",
89
+ httpUrl="https://api.example.com/http",
90
+ trust=True,
91
+ )
92
+
93
+ adapter = GeminiAdapter()
94
+ result = adapter.serialize(config)
95
+
96
+ self.assertEqual(result["httpUrl"], "https://api.example.com/http")
97
+ self.assertEqual(result["trust"], True)
98
+ self.assertNotIn("name", result)
99
+ self.assertNotIn("type", result)
100
+
101
+
102
+ class TestVSCodeAdapterSerialization(unittest.TestCase):
103
+ """Integration tests for VS Code adapter serialization."""
104
+
105
+ def test_AS05_vscode_with_envfile(self):
106
+ """AS-05: VS Code config with envFile serializes correctly."""
107
+ config = MCPServerConfig(
108
+ name="vscode-server",
109
+ command="node",
110
+ args=["server.js"],
111
+ envFile=".env",
112
+ type="stdio",
113
+ )
114
+
115
+ adapter = VSCodeAdapter()
116
+ result = adapter.serialize(config)
117
+
118
+ self.assertEqual(result["command"], "node")
119
+ self.assertEqual(result["envFile"], ".env")
120
+ self.assertEqual(result["type"], "stdio")
121
+ self.assertNotIn("name", result)
122
+
123
+
124
+ class TestCodexAdapterSerialization(unittest.TestCase):
125
+ """Integration tests for Codex adapter serialization."""
126
+
127
+ def test_AS06_codex_stdio_serialization(self):
128
+ """AS-06: Codex stdio config serializes correctly (no type field).
129
+
130
+ Note: Codex maps 'args' to 'arguments' and 'headers' to 'http_headers'.
131
+ """
132
+ config = MCPServerConfig(
133
+ name="codex-server",
134
+ command="python",
135
+ args=["server.py"],
136
+ env={"DEBUG": "true"},
137
+ )
138
+
139
+ adapter = CodexAdapter()
140
+ result = adapter.serialize(config)
141
+
142
+ self.assertEqual(result["command"], "python")
143
+ # Codex uses 'arguments' instead of 'args'
144
+ self.assertEqual(result["arguments"], ["server.py"])
145
+ self.assertNotIn("args", result) # Original name should not be present
146
+ self.assertEqual(result["env"], {"DEBUG": "true"})
147
+ self.assertNotIn("name", result)
148
+ self.assertNotIn("type", result)
149
+
150
+
151
+ class TestKiroAdapterSerialization(unittest.TestCase):
152
+ """Integration tests for Kiro adapter serialization."""
153
+
154
+ def test_AS07_kiro_stdio_serialization(self):
155
+ """AS-07: Kiro stdio config serializes correctly."""
156
+ config = MCPServerConfig(
157
+ name="kiro-server",
158
+ command="npx",
159
+ args=["@modelcontextprotocol/server"],
160
+ )
161
+
162
+ adapter = KiroAdapter()
163
+ result = adapter.serialize(config)
164
+
165
+ self.assertEqual(result["command"], "npx")
166
+ self.assertEqual(result["args"], ["@modelcontextprotocol/server"])
167
+ self.assertNotIn("name", result)
168
+ self.assertNotIn("type", result)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ unittest.main()
173
+
@@ -0,0 +1,16 @@
1
+ """Regression tests for CLI reporter infrastructure.
2
+
3
+ This package contains regression tests for the CLI UX normalization components:
4
+ - ResultReporter state management and data integrity
5
+ - ConsequenceType enum contracts
6
+ - Color enable/disable logic
7
+ - Consequence dataclass invariants
8
+
9
+ These tests focus on behavioral contracts rather than output format strings,
10
+ ensuring the infrastructure works correctly regardless of UX iteration.
11
+
12
+ Test Groups:
13
+ test_result_reporter.py: ResultReporter state management, Consequence nesting
14
+ test_consequence_type.py: ConsequenceType enum completeness and properties
15
+ test_color_logic.py: Color enum and enable/disable decision logic
16
+ """
@@ -0,0 +1,268 @@
1
+ """Regression tests for Color enum and color enable/disable logic.
2
+
3
+ This module tests:
4
+ - Color enum completeness (all 14 values defined)
5
+ - Color enable/disable decision logic (TTY, NO_COLOR)
6
+
7
+ Reference: R05 §3.4 (05-test_definition_v0.md)
8
+
9
+ Test Groups:
10
+ TestColorEnum: Color enum completeness and ANSI code format
11
+ TestColorsEnabled: Color enable/disable decision logic
12
+ """
13
+
14
+ import os
15
+ import sys
16
+ import unittest
17
+ from unittest.mock import patch
18
+
19
+
20
+ class TestColorEnum(unittest.TestCase):
21
+ """Tests for Color enum completeness and structure.
22
+
23
+ Reference: R06 §3.1 - Color interface contract
24
+ """
25
+
26
+ def test_color_enum_exists(self):
27
+ """Color enum should be importable from cli_utils."""
28
+ from hatch.cli.cli_utils import Color
29
+ self.assertTrue(hasattr(Color, '__members__'))
30
+
31
+ def test_color_enum_has_bright_colors(self):
32
+ """Color enum should have all 6 bright colors for results."""
33
+ from hatch.cli.cli_utils import Color
34
+
35
+ bright_colors = ['GREEN', 'RED', 'YELLOW', 'BLUE', 'MAGENTA', 'CYAN']
36
+ for color_name in bright_colors:
37
+ self.assertTrue(
38
+ hasattr(Color, color_name),
39
+ f"Color enum missing bright color: {color_name}"
40
+ )
41
+
42
+ def test_color_enum_has_dim_colors(self):
43
+ """Color enum should have all 6 dim colors for prompts."""
44
+ from hatch.cli.cli_utils import Color
45
+
46
+ dim_colors = [
47
+ 'GREEN_DIM', 'RED_DIM', 'YELLOW_DIM',
48
+ 'BLUE_DIM', 'MAGENTA_DIM', 'CYAN_DIM'
49
+ ]
50
+ for color_name in dim_colors:
51
+ self.assertTrue(
52
+ hasattr(Color, color_name),
53
+ f"Color enum missing dim color: {color_name}"
54
+ )
55
+
56
+ def test_color_enum_has_utility_colors(self):
57
+ """Color enum should have GRAY and RESET utility colors."""
58
+ from hatch.cli.cli_utils import Color
59
+
60
+ self.assertTrue(hasattr(Color, 'GRAY'), "Color enum missing GRAY")
61
+ self.assertTrue(hasattr(Color, 'RESET'), "Color enum missing RESET")
62
+
63
+ def test_color_enum_total_count(self):
64
+ """Color enum should have exactly 14 members."""
65
+ from hatch.cli.cli_utils import Color
66
+
67
+ # 6 bright + 6 dim + GRAY + RESET = 14
68
+ self.assertEqual(len(Color), 14, f"Expected 14 colors, got {len(Color)}")
69
+
70
+ def test_color_values_are_ansi_codes(self):
71
+ """Color values should be ANSI escape sequences (16-color or true color)."""
72
+ from hatch.cli.cli_utils import Color
73
+
74
+ for color in Color:
75
+ self.assertTrue(
76
+ color.value.startswith('\033['),
77
+ f"{color.name} value should start with ANSI escape: {repr(color.value)}"
78
+ )
79
+ self.assertTrue(
80
+ color.value.endswith('m'),
81
+ f"{color.name} value should end with 'm': {repr(color.value)}"
82
+ )
83
+ # Verify it's either 16-color or true color format
84
+ is_16_color = color.value.startswith('\033[') and not color.value.startswith('\033[38;2;')
85
+ is_true_color = color.value.startswith('\033[38;2;')
86
+ self.assertTrue(
87
+ is_16_color or is_true_color or color.name == 'RESET',
88
+ f"{color.name} should be 16-color or true color format: {repr(color.value)}"
89
+ )
90
+
91
+ def test_amber_color_exists(self):
92
+ """Color.AMBER should exist for entity highlighting."""
93
+ from hatch.cli.cli_utils import Color
94
+
95
+ self.assertTrue(
96
+ hasattr(Color, 'AMBER'),
97
+ "Color enum missing AMBER for entity highlighting"
98
+ )
99
+ # AMBER should have a valid ANSI value
100
+ self.assertTrue(
101
+ Color.AMBER.value.startswith('\033['),
102
+ f"AMBER value should be ANSI escape: {repr(Color.AMBER.value)}"
103
+ )
104
+
105
+ def test_reset_clears_formatting(self):
106
+ """RESET should be the standard ANSI reset code."""
107
+ from hatch.cli.cli_utils import Color
108
+
109
+ self.assertEqual(Color.RESET.value, '\033[0m')
110
+
111
+
112
+ class TestTrueColorDetection(unittest.TestCase):
113
+ """Tests for true color (24-bit) terminal detection.
114
+
115
+ Reference: R12 §7.2 (12-enhancing_colors_v0.md) - True color detection tests
116
+ """
117
+
118
+ def test_truecolor_detection_colorterm_truecolor(self):
119
+ """True color should be detected when COLORTERM=truecolor."""
120
+ from hatch.cli.cli_utils import _supports_truecolor
121
+
122
+ with patch.dict(os.environ, {'COLORTERM': 'truecolor'}, clear=True):
123
+ self.assertTrue(_supports_truecolor())
124
+
125
+ def test_truecolor_detection_colorterm_24bit(self):
126
+ """True color should be detected when COLORTERM=24bit."""
127
+ from hatch.cli.cli_utils import _supports_truecolor
128
+
129
+ with patch.dict(os.environ, {'COLORTERM': '24bit'}, clear=True):
130
+ self.assertTrue(_supports_truecolor())
131
+
132
+ def test_truecolor_detection_term_program_iterm(self):
133
+ """True color should be detected for iTerm.app."""
134
+ from hatch.cli.cli_utils import _supports_truecolor
135
+
136
+ with patch.dict(os.environ, {'TERM_PROGRAM': 'iTerm.app'}, clear=True):
137
+ self.assertTrue(_supports_truecolor())
138
+
139
+ def test_truecolor_detection_term_program_vscode(self):
140
+ """True color should be detected for VS Code terminal."""
141
+ from hatch.cli.cli_utils import _supports_truecolor
142
+
143
+ with patch.dict(os.environ, {'TERM_PROGRAM': 'vscode'}, clear=True):
144
+ self.assertTrue(_supports_truecolor())
145
+
146
+ def test_truecolor_detection_windows_terminal(self):
147
+ """True color should be detected for Windows Terminal (WT_SESSION)."""
148
+ from hatch.cli.cli_utils import _supports_truecolor
149
+
150
+ with patch.dict(os.environ, {'WT_SESSION': 'some-session-id'}, clear=True):
151
+ self.assertTrue(_supports_truecolor())
152
+
153
+ def test_truecolor_detection_fallback_false(self):
154
+ """True color should return False when no indicators present."""
155
+ from hatch.cli.cli_utils import _supports_truecolor
156
+
157
+ # Clear all true color indicators
158
+ clean_env = {}
159
+ with patch.dict(os.environ, clean_env, clear=True):
160
+ self.assertFalse(_supports_truecolor())
161
+
162
+
163
+ class TestHighlightFunction(unittest.TestCase):
164
+ """Tests for highlight() utility function.
165
+
166
+ Reference: R12 §3.3 (12-enhancing_colors_v0.md) - Bold modifier
167
+ """
168
+
169
+ def test_highlight_with_colors_enabled(self):
170
+ """highlight() should apply bold + amber when colors enabled."""
171
+ from hatch.cli.cli_utils import highlight, Color
172
+
173
+ env_without_no_color = {k: v for k, v in os.environ.items() if k != 'NO_COLOR'}
174
+ with patch.dict(os.environ, env_without_no_color, clear=True):
175
+ with patch.object(sys.stdout, 'isatty', return_value=True):
176
+ result = highlight('test-entity')
177
+
178
+ # Should contain bold escape
179
+ self.assertIn('\033[1m', result)
180
+ # Should contain amber color
181
+ self.assertIn(Color.AMBER.value, result)
182
+ # Should contain reset
183
+ self.assertIn(Color.RESET.value, result)
184
+ # Should contain the text
185
+ self.assertIn('test-entity', result)
186
+
187
+ def test_highlight_with_colors_disabled(self):
188
+ """highlight() should return plain text when colors disabled."""
189
+ from hatch.cli.cli_utils import highlight
190
+
191
+ with patch.dict(os.environ, {'NO_COLOR': '1'}):
192
+ result = highlight('test-entity')
193
+
194
+ # Should be plain text without ANSI codes
195
+ self.assertEqual(result, 'test-entity')
196
+ self.assertNotIn('\033[', result)
197
+
198
+ def test_highlight_non_tty(self):
199
+ """highlight() should return plain text in non-TTY mode."""
200
+ from hatch.cli.cli_utils import highlight
201
+
202
+ env_without_no_color = {k: v for k, v in os.environ.items() if k != 'NO_COLOR'}
203
+ with patch.dict(os.environ, env_without_no_color, clear=True):
204
+ with patch.object(sys.stdout, 'isatty', return_value=False):
205
+ result = highlight('test-entity')
206
+
207
+ # Should be plain text
208
+ self.assertEqual(result, 'test-entity')
209
+
210
+
211
+ class TestColorsEnabled(unittest.TestCase):
212
+ """Tests for color enable/disable decision logic.
213
+
214
+ Reference: R05 §3.4 - Color Enable/Disable Logic test group
215
+ """
216
+
217
+ def test_colors_disabled_when_no_color_set(self):
218
+ """Colors should be disabled when NO_COLOR=1."""
219
+ from hatch.cli.cli_utils import _colors_enabled
220
+
221
+ with patch.dict(os.environ, {'NO_COLOR': '1'}):
222
+ self.assertFalse(_colors_enabled())
223
+
224
+ def test_colors_disabled_when_no_color_truthy(self):
225
+ """Colors should be disabled when NO_COLOR=true."""
226
+ from hatch.cli.cli_utils import _colors_enabled
227
+
228
+ with patch.dict(os.environ, {'NO_COLOR': 'true'}):
229
+ self.assertFalse(_colors_enabled())
230
+
231
+ def test_colors_enabled_when_no_color_empty(self):
232
+ """Colors should be enabled when NO_COLOR is empty string (if TTY)."""
233
+ from hatch.cli.cli_utils import _colors_enabled
234
+
235
+ with patch.dict(os.environ, {'NO_COLOR': ''}, clear=False):
236
+ with patch.object(sys.stdout, 'isatty', return_value=True):
237
+ self.assertTrue(_colors_enabled())
238
+
239
+ def test_colors_enabled_when_no_color_unset(self):
240
+ """Colors should be enabled when NO_COLOR is not set (if TTY)."""
241
+ from hatch.cli.cli_utils import _colors_enabled
242
+
243
+ env_without_no_color = {k: v for k, v in os.environ.items() if k != 'NO_COLOR'}
244
+ with patch.dict(os.environ, env_without_no_color, clear=True):
245
+ with patch.object(sys.stdout, 'isatty', return_value=True):
246
+ self.assertTrue(_colors_enabled())
247
+
248
+ def test_colors_disabled_when_not_tty(self):
249
+ """Colors should be disabled when stdout is not a TTY."""
250
+ from hatch.cli.cli_utils import _colors_enabled
251
+
252
+ env_without_no_color = {k: v for k, v in os.environ.items() if k != 'NO_COLOR'}
253
+ with patch.dict(os.environ, env_without_no_color, clear=True):
254
+ with patch.object(sys.stdout, 'isatty', return_value=False):
255
+ self.assertFalse(_colors_enabled())
256
+
257
+ def test_colors_enabled_when_tty_and_no_no_color(self):
258
+ """Colors should be enabled when TTY and NO_COLOR not set."""
259
+ from hatch.cli.cli_utils import _colors_enabled
260
+
261
+ env_without_no_color = {k: v for k, v in os.environ.items() if k != 'NO_COLOR'}
262
+ with patch.dict(os.environ, env_without_no_color, clear=True):
263
+ with patch.object(sys.stdout, 'isatty', return_value=True):
264
+ self.assertTrue(_colors_enabled())
265
+
266
+
267
+ if __name__ == '__main__':
268
+ unittest.main()