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.
- hatch/__init__.py +1 -1
- hatch/cli/__init__.py +71 -0
- hatch/cli/__main__.py +1035 -0
- hatch/cli/cli_env.py +865 -0
- hatch/cli/cli_mcp.py +1965 -0
- hatch/cli/cli_package.py +566 -0
- hatch/cli/cli_system.py +136 -0
- hatch/cli/cli_utils.py +1289 -0
- hatch/cli_hatch.py +160 -2838
- hatch/mcp_host_config/__init__.py +10 -10
- hatch/mcp_host_config/adapters/__init__.py +34 -0
- hatch/mcp_host_config/adapters/base.py +170 -0
- hatch/mcp_host_config/adapters/claude.py +105 -0
- hatch/mcp_host_config/adapters/codex.py +104 -0
- hatch/mcp_host_config/adapters/cursor.py +83 -0
- hatch/mcp_host_config/adapters/gemini.py +75 -0
- hatch/mcp_host_config/adapters/kiro.py +78 -0
- hatch/mcp_host_config/adapters/lmstudio.py +79 -0
- hatch/mcp_host_config/adapters/registry.py +149 -0
- hatch/mcp_host_config/adapters/vscode.py +83 -0
- hatch/mcp_host_config/backup.py +5 -3
- hatch/mcp_host_config/fields.py +126 -0
- hatch/mcp_host_config/models.py +161 -456
- hatch/mcp_host_config/reporting.py +57 -16
- hatch/mcp_host_config/strategies.py +155 -87
- hatch/template_generator.py +1 -1
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/METADATA +3 -2
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/RECORD +52 -43
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/WHEEL +1 -1
- hatch_xclam-0.8.0.dev1.dist-info/entry_points.txt +2 -0
- tests/cli_test_utils.py +280 -0
- tests/integration/cli/__init__.py +14 -0
- tests/integration/cli/test_cli_reporter_integration.py +2439 -0
- tests/integration/mcp/__init__.py +0 -0
- tests/integration/mcp/test_adapter_serialization.py +173 -0
- tests/regression/cli/__init__.py +16 -0
- tests/regression/cli/test_color_logic.py +268 -0
- tests/regression/cli/test_consequence_type.py +298 -0
- tests/regression/cli/test_error_formatting.py +328 -0
- tests/regression/cli/test_result_reporter.py +586 -0
- tests/regression/cli/test_table_formatter.py +211 -0
- tests/regression/mcp/__init__.py +0 -0
- tests/regression/mcp/test_field_filtering.py +162 -0
- tests/test_cli_version.py +7 -5
- tests/test_data/fixtures/cli_reporter_fixtures.py +184 -0
- tests/unit/__init__.py +0 -0
- tests/unit/mcp/__init__.py +0 -0
- tests/unit/mcp/test_adapter_protocol.py +138 -0
- tests/unit/mcp/test_adapter_registry.py +158 -0
- tests/unit/mcp/test_config_model.py +146 -0
- hatch_xclam-0.7.1.dev3.dist-info/entry_points.txt +0 -2
- tests/integration/test_mcp_kiro_integration.py +0 -153
- tests/regression/test_mcp_codex_backup_integration.py +0 -162
- tests/regression/test_mcp_codex_host_strategy.py +0 -163
- tests/regression/test_mcp_codex_model_validation.py +0 -117
- tests/regression/test_mcp_kiro_backup_integration.py +0 -241
- tests/regression/test_mcp_kiro_cli_integration.py +0 -141
- tests/regression/test_mcp_kiro_decorator_registration.py +0 -71
- tests/regression/test_mcp_kiro_host_strategy.py +0 -214
- tests/regression/test_mcp_kiro_model_validation.py +0 -116
- tests/regression/test_mcp_kiro_omni_conversion.py +0 -104
- tests/test_mcp_atomic_operations.py +0 -276
- tests/test_mcp_backup_integration.py +0 -308
- tests/test_mcp_cli_all_host_specific_args.py +0 -496
- tests/test_mcp_cli_backup_management.py +0 -295
- tests/test_mcp_cli_direct_management.py +0 -456
- tests/test_mcp_cli_discovery_listing.py +0 -582
- tests/test_mcp_cli_host_config_integration.py +0 -823
- tests/test_mcp_cli_package_management.py +0 -360
- tests/test_mcp_cli_partial_updates.py +0 -859
- tests/test_mcp_environment_integration.py +0 -520
- tests/test_mcp_host_config_backup.py +0 -257
- tests/test_mcp_host_configuration_manager.py +0 -331
- tests/test_mcp_host_registry_decorator.py +0 -348
- tests/test_mcp_pydantic_architecture_v4.py +0 -603
- tests/test_mcp_server_config_models.py +0 -242
- tests/test_mcp_server_config_type_field.py +0 -221
- tests/test_mcp_sync_functionality.py +0 -316
- tests/test_mcp_user_feedback_reporting.py +0 -359
- {hatch_xclam-0.7.1.dev3.dist-info → hatch_xclam-0.8.0.dev1.dist-info}/licenses/LICENSE +0 -0
- {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()
|