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
@@ -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")