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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""Regression tests for ConsequenceType enum.
|
|
2
|
+
|
|
3
|
+
This module tests:
|
|
4
|
+
- ConsequenceType enum completeness (all 16 types defined)
|
|
5
|
+
- Tense-aware label properties (prompt_label, result_label)
|
|
6
|
+
- Color properties (prompt_color, result_color)
|
|
7
|
+
- Irregular verb handling (SET, EXISTS, UNCHANGED)
|
|
8
|
+
|
|
9
|
+
Reference: R05 §3.2 (05-test_definition_v0.md)
|
|
10
|
+
Reference: R06 §3.2 (06-dependency_analysis_v0.md)
|
|
11
|
+
Reference: R03 §2 (03-mutation_output_specification_v0.md)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import unittest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestConsequenceTypeEnum(unittest.TestCase):
|
|
18
|
+
"""Tests for ConsequenceType enum completeness and structure.
|
|
19
|
+
|
|
20
|
+
Reference: R06 §3.2 - ConsequenceType interface contract
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def test_consequence_type_enum_exists(self):
|
|
24
|
+
"""ConsequenceType enum should be importable from cli_utils."""
|
|
25
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
26
|
+
self.assertTrue(hasattr(ConsequenceType, '__members__'))
|
|
27
|
+
|
|
28
|
+
def test_consequence_type_has_all_constructive_types(self):
|
|
29
|
+
"""ConsequenceType should have all constructive action types (Green)."""
|
|
30
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
31
|
+
|
|
32
|
+
constructive_types = ['CREATE', 'ADD', 'CONFIGURE', 'INSTALL', 'INITIALIZE']
|
|
33
|
+
for type_name in constructive_types:
|
|
34
|
+
self.assertTrue(
|
|
35
|
+
hasattr(ConsequenceType, type_name),
|
|
36
|
+
f"ConsequenceType missing constructive type: {type_name}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def test_consequence_type_has_recovery_type(self):
|
|
40
|
+
"""ConsequenceType should have RESTORE recovery type (Blue)."""
|
|
41
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
42
|
+
self.assertTrue(hasattr(ConsequenceType, 'RESTORE'))
|
|
43
|
+
|
|
44
|
+
def test_consequence_type_has_all_destructive_types(self):
|
|
45
|
+
"""ConsequenceType should have all destructive action types (Red)."""
|
|
46
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
47
|
+
|
|
48
|
+
destructive_types = ['REMOVE', 'DELETE', 'CLEAN']
|
|
49
|
+
for type_name in destructive_types:
|
|
50
|
+
self.assertTrue(
|
|
51
|
+
hasattr(ConsequenceType, type_name),
|
|
52
|
+
f"ConsequenceType missing destructive type: {type_name}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def test_consequence_type_has_all_modification_types(self):
|
|
56
|
+
"""ConsequenceType should have all modification action types (Yellow)."""
|
|
57
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
58
|
+
|
|
59
|
+
modification_types = ['SET', 'UPDATE']
|
|
60
|
+
for type_name in modification_types:
|
|
61
|
+
self.assertTrue(
|
|
62
|
+
hasattr(ConsequenceType, type_name),
|
|
63
|
+
f"ConsequenceType missing modification type: {type_name}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def test_consequence_type_has_transfer_type(self):
|
|
67
|
+
"""ConsequenceType should have SYNC transfer type (Magenta)."""
|
|
68
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
69
|
+
self.assertTrue(hasattr(ConsequenceType, 'SYNC'))
|
|
70
|
+
|
|
71
|
+
def test_consequence_type_has_informational_type(self):
|
|
72
|
+
"""ConsequenceType should have VALIDATE informational type (Cyan)."""
|
|
73
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
74
|
+
self.assertTrue(hasattr(ConsequenceType, 'VALIDATE'))
|
|
75
|
+
|
|
76
|
+
def test_consequence_type_has_all_noop_types(self):
|
|
77
|
+
"""ConsequenceType should have all no-op action types (Gray)."""
|
|
78
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
79
|
+
|
|
80
|
+
noop_types = ['SKIP', 'EXISTS', 'UNCHANGED']
|
|
81
|
+
for type_name in noop_types:
|
|
82
|
+
self.assertTrue(
|
|
83
|
+
hasattr(ConsequenceType, type_name),
|
|
84
|
+
f"ConsequenceType missing no-op type: {type_name}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def test_consequence_type_total_count(self):
|
|
88
|
+
"""ConsequenceType should have exactly 16 members."""
|
|
89
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
90
|
+
|
|
91
|
+
# 5 constructive + 1 recovery + 3 destructive + 2 modification +
|
|
92
|
+
# 1 transfer + 1 informational + 3 noop = 16
|
|
93
|
+
self.assertEqual(
|
|
94
|
+
len(ConsequenceType), 16,
|
|
95
|
+
f"Expected 16 consequence types, got {len(ConsequenceType)}"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class TestConsequenceTypeProperties(unittest.TestCase):
|
|
100
|
+
"""Tests for ConsequenceType tense-aware properties.
|
|
101
|
+
|
|
102
|
+
Reference: R05 §3.2 - ConsequenceType Behavior test group
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
def test_all_types_have_prompt_label(self):
|
|
106
|
+
"""All ConsequenceType members should have prompt_label property."""
|
|
107
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
108
|
+
|
|
109
|
+
for ct in ConsequenceType:
|
|
110
|
+
self.assertTrue(
|
|
111
|
+
hasattr(ct, 'prompt_label'),
|
|
112
|
+
f"{ct.name} missing prompt_label property"
|
|
113
|
+
)
|
|
114
|
+
self.assertIsInstance(ct.prompt_label, str)
|
|
115
|
+
self.assertTrue(
|
|
116
|
+
len(ct.prompt_label) > 0,
|
|
117
|
+
f"{ct.name}.prompt_label should not be empty"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def test_all_types_have_result_label(self):
|
|
121
|
+
"""All ConsequenceType members should have result_label property."""
|
|
122
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
123
|
+
|
|
124
|
+
for ct in ConsequenceType:
|
|
125
|
+
self.assertTrue(
|
|
126
|
+
hasattr(ct, 'result_label'),
|
|
127
|
+
f"{ct.name} missing result_label property"
|
|
128
|
+
)
|
|
129
|
+
self.assertIsInstance(ct.result_label, str)
|
|
130
|
+
self.assertTrue(
|
|
131
|
+
len(ct.result_label) > 0,
|
|
132
|
+
f"{ct.name}.result_label should not be empty"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def test_all_types_have_prompt_color(self):
|
|
136
|
+
"""All ConsequenceType members should have prompt_color property."""
|
|
137
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
138
|
+
|
|
139
|
+
for ct in ConsequenceType:
|
|
140
|
+
self.assertTrue(
|
|
141
|
+
hasattr(ct, 'prompt_color'),
|
|
142
|
+
f"{ct.name} missing prompt_color property"
|
|
143
|
+
)
|
|
144
|
+
self.assertIsInstance(ct.prompt_color, Color)
|
|
145
|
+
|
|
146
|
+
def test_all_types_have_result_color(self):
|
|
147
|
+
"""All ConsequenceType members should have result_color property."""
|
|
148
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
149
|
+
|
|
150
|
+
for ct in ConsequenceType:
|
|
151
|
+
self.assertTrue(
|
|
152
|
+
hasattr(ct, 'result_color'),
|
|
153
|
+
f"{ct.name} missing result_color property"
|
|
154
|
+
)
|
|
155
|
+
self.assertIsInstance(ct.result_color, Color)
|
|
156
|
+
|
|
157
|
+
def test_irregular_verbs_prompt_equals_result(self):
|
|
158
|
+
"""Irregular verbs (SET, EXISTS, UNCHANGED) should have same prompt and result labels."""
|
|
159
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
160
|
+
|
|
161
|
+
irregular_verbs = [
|
|
162
|
+
ConsequenceType.SET,
|
|
163
|
+
ConsequenceType.EXISTS,
|
|
164
|
+
ConsequenceType.UNCHANGED,
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
for ct in irregular_verbs:
|
|
168
|
+
self.assertEqual(
|
|
169
|
+
ct.prompt_label, ct.result_label,
|
|
170
|
+
f"{ct.name} is irregular: prompt_label should equal result_label"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def test_regular_verbs_result_ends_with_ed(self):
|
|
174
|
+
"""Regular verbs should have result_label ending with 'ED'."""
|
|
175
|
+
from hatch.cli.cli_utils import ConsequenceType
|
|
176
|
+
|
|
177
|
+
# Irregular verbs that don't follow -ED pattern
|
|
178
|
+
irregular = {'SET', 'EXISTS', 'UNCHANGED'}
|
|
179
|
+
|
|
180
|
+
for ct in ConsequenceType:
|
|
181
|
+
if ct.name not in irregular:
|
|
182
|
+
self.assertTrue(
|
|
183
|
+
ct.result_label.endswith('ED'),
|
|
184
|
+
f"{ct.name}.result_label '{ct.result_label}' should end with 'ED'"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestConsequenceTypeColorSemantics(unittest.TestCase):
|
|
189
|
+
"""Tests for ConsequenceType color semantic correctness.
|
|
190
|
+
|
|
191
|
+
Reference: R03 §4.3 - Verb-to-Color mapping
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
def test_constructive_types_use_green(self):
|
|
195
|
+
"""Constructive types should use green colors."""
|
|
196
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
197
|
+
|
|
198
|
+
constructive = [
|
|
199
|
+
ConsequenceType.CREATE,
|
|
200
|
+
ConsequenceType.ADD,
|
|
201
|
+
ConsequenceType.CONFIGURE,
|
|
202
|
+
ConsequenceType.INSTALL,
|
|
203
|
+
ConsequenceType.INITIALIZE,
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
for ct in constructive:
|
|
207
|
+
self.assertEqual(
|
|
208
|
+
ct.prompt_color, Color.GREEN_DIM,
|
|
209
|
+
f"{ct.name} prompt_color should be GREEN_DIM"
|
|
210
|
+
)
|
|
211
|
+
self.assertEqual(
|
|
212
|
+
ct.result_color, Color.GREEN,
|
|
213
|
+
f"{ct.name} result_color should be GREEN"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def test_recovery_type_uses_blue(self):
|
|
217
|
+
"""RESTORE should use blue colors."""
|
|
218
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
219
|
+
|
|
220
|
+
self.assertEqual(ConsequenceType.RESTORE.prompt_color, Color.BLUE_DIM)
|
|
221
|
+
self.assertEqual(ConsequenceType.RESTORE.result_color, Color.BLUE)
|
|
222
|
+
|
|
223
|
+
def test_destructive_types_use_red(self):
|
|
224
|
+
"""Destructive types should use red colors."""
|
|
225
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
226
|
+
|
|
227
|
+
destructive = [
|
|
228
|
+
ConsequenceType.REMOVE,
|
|
229
|
+
ConsequenceType.DELETE,
|
|
230
|
+
ConsequenceType.CLEAN,
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
for ct in destructive:
|
|
234
|
+
self.assertEqual(
|
|
235
|
+
ct.prompt_color, Color.RED_DIM,
|
|
236
|
+
f"{ct.name} prompt_color should be RED_DIM"
|
|
237
|
+
)
|
|
238
|
+
self.assertEqual(
|
|
239
|
+
ct.result_color, Color.RED,
|
|
240
|
+
f"{ct.name} result_color should be RED"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def test_modification_types_use_yellow(self):
|
|
244
|
+
"""Modification types should use yellow colors."""
|
|
245
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
246
|
+
|
|
247
|
+
modification = [
|
|
248
|
+
ConsequenceType.SET,
|
|
249
|
+
ConsequenceType.UPDATE,
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
for ct in modification:
|
|
253
|
+
self.assertEqual(
|
|
254
|
+
ct.prompt_color, Color.YELLOW_DIM,
|
|
255
|
+
f"{ct.name} prompt_color should be YELLOW_DIM"
|
|
256
|
+
)
|
|
257
|
+
self.assertEqual(
|
|
258
|
+
ct.result_color, Color.YELLOW,
|
|
259
|
+
f"{ct.name} result_color should be YELLOW"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def test_transfer_type_uses_magenta(self):
|
|
263
|
+
"""SYNC should use magenta colors."""
|
|
264
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
265
|
+
|
|
266
|
+
self.assertEqual(ConsequenceType.SYNC.prompt_color, Color.MAGENTA_DIM)
|
|
267
|
+
self.assertEqual(ConsequenceType.SYNC.result_color, Color.MAGENTA)
|
|
268
|
+
|
|
269
|
+
def test_informational_type_uses_cyan(self):
|
|
270
|
+
"""VALIDATE should use cyan colors."""
|
|
271
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
272
|
+
|
|
273
|
+
self.assertEqual(ConsequenceType.VALIDATE.prompt_color, Color.CYAN_DIM)
|
|
274
|
+
self.assertEqual(ConsequenceType.VALIDATE.result_color, Color.CYAN)
|
|
275
|
+
|
|
276
|
+
def test_noop_types_use_gray(self):
|
|
277
|
+
"""No-op types should use gray colors (same for prompt and result)."""
|
|
278
|
+
from hatch.cli.cli_utils import ConsequenceType, Color
|
|
279
|
+
|
|
280
|
+
noop = [
|
|
281
|
+
ConsequenceType.SKIP,
|
|
282
|
+
ConsequenceType.EXISTS,
|
|
283
|
+
ConsequenceType.UNCHANGED,
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
for ct in noop:
|
|
287
|
+
self.assertEqual(
|
|
288
|
+
ct.prompt_color, Color.GRAY,
|
|
289
|
+
f"{ct.name} prompt_color should be GRAY"
|
|
290
|
+
)
|
|
291
|
+
self.assertEqual(
|
|
292
|
+
ct.result_color, Color.GRAY,
|
|
293
|
+
f"{ct.name} result_color should be GRAY"
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
if __name__ == '__main__':
|
|
298
|
+
unittest.main()
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Regression tests for error formatting infrastructure.
|
|
2
|
+
|
|
3
|
+
This module tests:
|
|
4
|
+
- HatchArgumentParser error formatting
|
|
5
|
+
- ValidationError exception class
|
|
6
|
+
- format_validation_error utility
|
|
7
|
+
- format_info utility
|
|
8
|
+
|
|
9
|
+
Reference: R13 §4.2.1 (13-error_message_formatting_v0.md) - HatchArgumentParser
|
|
10
|
+
Reference: R13 §4.2.2 (13-error_message_formatting_v0.md) - ValidationError
|
|
11
|
+
Reference: R13 §4.3 (13-error_message_formatting_v0.md) - Utilities
|
|
12
|
+
Reference: R13 §6.1 (13-error_message_formatting_v0.md) - Argparse error catalog
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import unittest
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestHatchArgumentParser(unittest.TestCase):
|
|
21
|
+
"""Tests for HatchArgumentParser error formatting.
|
|
22
|
+
|
|
23
|
+
Reference: R13 §4.2.1 - Custom ArgumentParser
|
|
24
|
+
Reference: R13 §6.1 - Argparse error catalog
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def test_argparse_error_has_error_prefix(self):
|
|
28
|
+
"""Argparse errors should have [ERROR] prefix."""
|
|
29
|
+
from hatch.cli.__main__ import HatchArgumentParser
|
|
30
|
+
import io
|
|
31
|
+
|
|
32
|
+
parser = HatchArgumentParser(prog="test")
|
|
33
|
+
|
|
34
|
+
# Capture stderr
|
|
35
|
+
captured = io.StringIO()
|
|
36
|
+
try:
|
|
37
|
+
parser.error("test error message")
|
|
38
|
+
except SystemExit:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# The error method writes to stderr and exits
|
|
42
|
+
# We need to test via subprocess for proper capture
|
|
43
|
+
result = subprocess.run(
|
|
44
|
+
[sys.executable, "-c",
|
|
45
|
+
"from hatch.cli.__main__ import HatchArgumentParser; "
|
|
46
|
+
"p = HatchArgumentParser(); p.error('test error')"],
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.assertIn("[ERROR]", result.stderr)
|
|
52
|
+
|
|
53
|
+
def test_argparse_error_unrecognized_argument(self):
|
|
54
|
+
"""Unrecognized argument error should have [ERROR] prefix."""
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[sys.executable, "-m", "hatch.cli", "--invalid-arg"],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.assertIn("[ERROR]", result.stderr)
|
|
62
|
+
self.assertIn("unrecognized arguments", result.stderr)
|
|
63
|
+
|
|
64
|
+
def test_argparse_error_exit_code_2(self):
|
|
65
|
+
"""Argparse errors should exit with code 2."""
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
[sys.executable, "-m", "hatch.cli", "--invalid-arg"],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.assertEqual(result.returncode, 2)
|
|
73
|
+
|
|
74
|
+
def test_argparse_error_no_ansi_in_pipe(self):
|
|
75
|
+
"""Argparse errors should not have ANSI codes when piped."""
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
[sys.executable, "-m", "hatch.cli", "--invalid-arg"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# When piped (capture_output=True), stdout is not a TTY
|
|
83
|
+
# so ANSI codes should not be present
|
|
84
|
+
self.assertNotIn("\033[", result.stderr)
|
|
85
|
+
|
|
86
|
+
def test_hatch_argument_parser_class_exists(self):
|
|
87
|
+
"""HatchArgumentParser class should be importable."""
|
|
88
|
+
from hatch.cli.__main__ import HatchArgumentParser
|
|
89
|
+
import argparse
|
|
90
|
+
|
|
91
|
+
self.assertTrue(issubclass(HatchArgumentParser, argparse.ArgumentParser))
|
|
92
|
+
|
|
93
|
+
def test_hatch_argument_parser_has_error_method(self):
|
|
94
|
+
"""HatchArgumentParser should have overridden error method."""
|
|
95
|
+
from hatch.cli.__main__ import HatchArgumentParser
|
|
96
|
+
import argparse
|
|
97
|
+
|
|
98
|
+
parser = HatchArgumentParser()
|
|
99
|
+
|
|
100
|
+
# Check that error method is overridden (not the same as base class)
|
|
101
|
+
self.assertIsNot(
|
|
102
|
+
HatchArgumentParser.error,
|
|
103
|
+
argparse.ArgumentParser.error
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == '__main__':
|
|
108
|
+
unittest.main()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestValidationError(unittest.TestCase):
|
|
112
|
+
"""Tests for ValidationError exception class.
|
|
113
|
+
|
|
114
|
+
Reference: R13 §4.2.2 - ValidationError interface
|
|
115
|
+
Reference: R13 §7.2 - ValidationError contract
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def test_validation_error_attributes(self):
|
|
119
|
+
"""ValidationError should have message, field, and suggestion attributes."""
|
|
120
|
+
from hatch.cli.cli_utils import ValidationError
|
|
121
|
+
|
|
122
|
+
error = ValidationError(
|
|
123
|
+
"Test message",
|
|
124
|
+
field="--host",
|
|
125
|
+
suggestion="Use valid host"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.assertEqual(error.message, "Test message")
|
|
129
|
+
self.assertEqual(error.field, "--host")
|
|
130
|
+
self.assertEqual(error.suggestion, "Use valid host")
|
|
131
|
+
|
|
132
|
+
def test_validation_error_str_returns_message(self):
|
|
133
|
+
"""ValidationError str() should return message."""
|
|
134
|
+
from hatch.cli.cli_utils import ValidationError
|
|
135
|
+
|
|
136
|
+
error = ValidationError("Test message")
|
|
137
|
+
self.assertEqual(str(error), "Test message")
|
|
138
|
+
|
|
139
|
+
def test_validation_error_optional_field(self):
|
|
140
|
+
"""ValidationError field should be optional."""
|
|
141
|
+
from hatch.cli.cli_utils import ValidationError
|
|
142
|
+
|
|
143
|
+
error = ValidationError("Test message")
|
|
144
|
+
self.assertIsNone(error.field)
|
|
145
|
+
|
|
146
|
+
def test_validation_error_optional_suggestion(self):
|
|
147
|
+
"""ValidationError suggestion should be optional."""
|
|
148
|
+
from hatch.cli.cli_utils import ValidationError
|
|
149
|
+
|
|
150
|
+
error = ValidationError("Test message")
|
|
151
|
+
self.assertIsNone(error.suggestion)
|
|
152
|
+
|
|
153
|
+
def test_validation_error_is_exception(self):
|
|
154
|
+
"""ValidationError should be an Exception subclass."""
|
|
155
|
+
from hatch.cli.cli_utils import ValidationError
|
|
156
|
+
|
|
157
|
+
self.assertTrue(issubclass(ValidationError, Exception))
|
|
158
|
+
|
|
159
|
+
def test_validation_error_can_be_raised(self):
|
|
160
|
+
"""ValidationError should be raisable."""
|
|
161
|
+
from hatch.cli.cli_utils import ValidationError
|
|
162
|
+
|
|
163
|
+
with self.assertRaises(ValidationError) as context:
|
|
164
|
+
raise ValidationError("Test error", field="--host")
|
|
165
|
+
|
|
166
|
+
self.assertEqual(context.exception.message, "Test error")
|
|
167
|
+
self.assertEqual(context.exception.field, "--host")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestFormatValidationError(unittest.TestCase):
|
|
171
|
+
"""Tests for format_validation_error utility.
|
|
172
|
+
|
|
173
|
+
Reference: R13 §4.3 - format_validation_error
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def test_format_validation_error_basic(self):
|
|
177
|
+
"""format_validation_error should print [ERROR] prefix."""
|
|
178
|
+
from hatch.cli.cli_utils import ValidationError, format_validation_error
|
|
179
|
+
import io
|
|
180
|
+
import sys
|
|
181
|
+
|
|
182
|
+
error = ValidationError("Test error message")
|
|
183
|
+
|
|
184
|
+
captured = io.StringIO()
|
|
185
|
+
sys.stdout = captured
|
|
186
|
+
try:
|
|
187
|
+
format_validation_error(error)
|
|
188
|
+
finally:
|
|
189
|
+
sys.stdout = sys.__stdout__
|
|
190
|
+
|
|
191
|
+
output = captured.getvalue()
|
|
192
|
+
self.assertIn("[ERROR]", output)
|
|
193
|
+
self.assertIn("Test error message", output)
|
|
194
|
+
|
|
195
|
+
def test_format_validation_error_with_field(self):
|
|
196
|
+
"""format_validation_error should print field if provided."""
|
|
197
|
+
from hatch.cli.cli_utils import ValidationError, format_validation_error
|
|
198
|
+
import io
|
|
199
|
+
import sys
|
|
200
|
+
|
|
201
|
+
error = ValidationError("Test error", field="--host")
|
|
202
|
+
|
|
203
|
+
captured = io.StringIO()
|
|
204
|
+
sys.stdout = captured
|
|
205
|
+
try:
|
|
206
|
+
format_validation_error(error)
|
|
207
|
+
finally:
|
|
208
|
+
sys.stdout = sys.__stdout__
|
|
209
|
+
|
|
210
|
+
output = captured.getvalue()
|
|
211
|
+
self.assertIn("Field: --host", output)
|
|
212
|
+
|
|
213
|
+
def test_format_validation_error_with_suggestion(self):
|
|
214
|
+
"""format_validation_error should print suggestion if provided."""
|
|
215
|
+
from hatch.cli.cli_utils import ValidationError, format_validation_error
|
|
216
|
+
import io
|
|
217
|
+
import sys
|
|
218
|
+
|
|
219
|
+
error = ValidationError("Test error", suggestion="Use valid host")
|
|
220
|
+
|
|
221
|
+
captured = io.StringIO()
|
|
222
|
+
sys.stdout = captured
|
|
223
|
+
try:
|
|
224
|
+
format_validation_error(error)
|
|
225
|
+
finally:
|
|
226
|
+
sys.stdout = sys.__stdout__
|
|
227
|
+
|
|
228
|
+
output = captured.getvalue()
|
|
229
|
+
self.assertIn("Suggestion: Use valid host", output)
|
|
230
|
+
|
|
231
|
+
def test_format_validation_error_full(self):
|
|
232
|
+
"""format_validation_error should print all fields when provided."""
|
|
233
|
+
from hatch.cli.cli_utils import ValidationError, format_validation_error
|
|
234
|
+
import io
|
|
235
|
+
import sys
|
|
236
|
+
|
|
237
|
+
error = ValidationError(
|
|
238
|
+
"Invalid host 'vsc'",
|
|
239
|
+
field="--host",
|
|
240
|
+
suggestion="Supported hosts: claude-desktop, vscode"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
captured = io.StringIO()
|
|
244
|
+
sys.stdout = captured
|
|
245
|
+
try:
|
|
246
|
+
format_validation_error(error)
|
|
247
|
+
finally:
|
|
248
|
+
sys.stdout = sys.__stdout__
|
|
249
|
+
|
|
250
|
+
output = captured.getvalue()
|
|
251
|
+
self.assertIn("[ERROR]", output)
|
|
252
|
+
self.assertIn("Invalid host 'vsc'", output)
|
|
253
|
+
self.assertIn("Field: --host", output)
|
|
254
|
+
self.assertIn("Suggestion: Supported hosts: claude-desktop, vscode", output)
|
|
255
|
+
|
|
256
|
+
def test_format_validation_error_no_color_in_non_tty(self):
|
|
257
|
+
"""format_validation_error should not include ANSI codes when not in TTY."""
|
|
258
|
+
from hatch.cli.cli_utils import ValidationError, format_validation_error
|
|
259
|
+
import io
|
|
260
|
+
import sys
|
|
261
|
+
|
|
262
|
+
error = ValidationError("Test error")
|
|
263
|
+
|
|
264
|
+
captured = io.StringIO()
|
|
265
|
+
sys.stdout = captured
|
|
266
|
+
try:
|
|
267
|
+
format_validation_error(error)
|
|
268
|
+
finally:
|
|
269
|
+
sys.stdout = sys.__stdout__
|
|
270
|
+
|
|
271
|
+
output = captured.getvalue()
|
|
272
|
+
self.assertNotIn("\033[", output)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TestFormatInfo(unittest.TestCase):
|
|
276
|
+
"""Tests for format_info utility.
|
|
277
|
+
|
|
278
|
+
Reference: R13-B §B.6.2 - Operation cancelled normalization
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
def test_format_info_basic(self):
|
|
282
|
+
"""format_info should print [INFO] prefix."""
|
|
283
|
+
from hatch.cli.cli_utils import format_info
|
|
284
|
+
import io
|
|
285
|
+
import sys
|
|
286
|
+
|
|
287
|
+
captured = io.StringIO()
|
|
288
|
+
sys.stdout = captured
|
|
289
|
+
try:
|
|
290
|
+
format_info("Operation cancelled")
|
|
291
|
+
finally:
|
|
292
|
+
sys.stdout = sys.__stdout__
|
|
293
|
+
|
|
294
|
+
output = captured.getvalue()
|
|
295
|
+
self.assertIn("[INFO]", output)
|
|
296
|
+
self.assertIn("Operation cancelled", output)
|
|
297
|
+
|
|
298
|
+
def test_format_info_no_color_in_non_tty(self):
|
|
299
|
+
"""format_info should not include ANSI codes when not in TTY."""
|
|
300
|
+
from hatch.cli.cli_utils import format_info
|
|
301
|
+
import io
|
|
302
|
+
import sys
|
|
303
|
+
|
|
304
|
+
captured = io.StringIO()
|
|
305
|
+
sys.stdout = captured
|
|
306
|
+
try:
|
|
307
|
+
format_info("Test message")
|
|
308
|
+
finally:
|
|
309
|
+
sys.stdout = sys.__stdout__
|
|
310
|
+
|
|
311
|
+
output = captured.getvalue()
|
|
312
|
+
self.assertNotIn("\033[", output)
|
|
313
|
+
|
|
314
|
+
def test_format_info_output_format(self):
|
|
315
|
+
"""format_info output should match expected format."""
|
|
316
|
+
from hatch.cli.cli_utils import format_info
|
|
317
|
+
import io
|
|
318
|
+
import sys
|
|
319
|
+
|
|
320
|
+
captured = io.StringIO()
|
|
321
|
+
sys.stdout = captured
|
|
322
|
+
try:
|
|
323
|
+
format_info("Test message")
|
|
324
|
+
finally:
|
|
325
|
+
sys.stdout = sys.__stdout__
|
|
326
|
+
|
|
327
|
+
output = captured.getvalue().strip()
|
|
328
|
+
self.assertEqual(output, "[INFO] Test message")
|