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,586 @@
|
|
|
1
|
+
"""Regression tests for Consequence dataclass and ResultReporter class.
|
|
2
|
+
|
|
3
|
+
This module tests:
|
|
4
|
+
- Consequence dataclass invariants and nesting support
|
|
5
|
+
- ResultReporter state management and consequence tracking
|
|
6
|
+
- ResultReporter mode flags (dry_run, command_name)
|
|
7
|
+
|
|
8
|
+
Reference: R05 §3.3 (05-test_definition_v0.md) - Nested Consequence Invariants
|
|
9
|
+
Reference: R05 §3.2 (05-test_definition_v0.md) - ResultReporter State Management
|
|
10
|
+
Reference: R06 §3.3, §3.4 (06-dependency_analysis_v0.md)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import unittest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestConsequence(unittest.TestCase):
|
|
17
|
+
"""Tests for Consequence dataclass invariants.
|
|
18
|
+
|
|
19
|
+
Reference: R06 §3.3 - Consequence interface contract
|
|
20
|
+
Reference: R04 §5.1 - Consequence data model invariants
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def test_consequence_dataclass_exists(self):
|
|
24
|
+
"""Consequence dataclass should be importable from cli_utils."""
|
|
25
|
+
from hatch.cli.cli_utils import Consequence
|
|
26
|
+
self.assertTrue(hasattr(Consequence, '__dataclass_fields__'))
|
|
27
|
+
|
|
28
|
+
def test_consequence_accepts_type_and_message(self):
|
|
29
|
+
"""Consequence should accept type and message arguments."""
|
|
30
|
+
from hatch.cli.cli_utils import Consequence, ConsequenceType
|
|
31
|
+
|
|
32
|
+
c = Consequence(type=ConsequenceType.CREATE, message="Test resource")
|
|
33
|
+
self.assertEqual(c.type, ConsequenceType.CREATE)
|
|
34
|
+
self.assertEqual(c.message, "Test resource")
|
|
35
|
+
|
|
36
|
+
def test_consequence_accepts_children_list(self):
|
|
37
|
+
"""Consequence should accept children list argument."""
|
|
38
|
+
from hatch.cli.cli_utils import Consequence, ConsequenceType
|
|
39
|
+
|
|
40
|
+
child1 = Consequence(type=ConsequenceType.UPDATE, message="field1: a → b")
|
|
41
|
+
child2 = Consequence(type=ConsequenceType.SKIP, message="field2: unsupported")
|
|
42
|
+
|
|
43
|
+
parent = Consequence(
|
|
44
|
+
type=ConsequenceType.CONFIGURE,
|
|
45
|
+
message="Server 'test'",
|
|
46
|
+
children=[child1, child2]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.assertEqual(len(parent.children), 2)
|
|
50
|
+
self.assertEqual(parent.children[0], child1)
|
|
51
|
+
self.assertEqual(parent.children[1], child2)
|
|
52
|
+
|
|
53
|
+
def test_consequence_default_children_is_empty_list(self):
|
|
54
|
+
"""Consequence should have empty list as default children."""
|
|
55
|
+
from hatch.cli.cli_utils import Consequence, ConsequenceType
|
|
56
|
+
|
|
57
|
+
c = Consequence(type=ConsequenceType.CREATE, message="Test")
|
|
58
|
+
self.assertEqual(c.children, [])
|
|
59
|
+
self.assertIsInstance(c.children, list)
|
|
60
|
+
|
|
61
|
+
def test_consequence_children_are_consequence_instances(self):
|
|
62
|
+
"""Children should be Consequence instances."""
|
|
63
|
+
from hatch.cli.cli_utils import Consequence, ConsequenceType
|
|
64
|
+
|
|
65
|
+
child = Consequence(type=ConsequenceType.UPDATE, message="child")
|
|
66
|
+
parent = Consequence(
|
|
67
|
+
type=ConsequenceType.CONFIGURE,
|
|
68
|
+
message="parent",
|
|
69
|
+
children=[child]
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
self.assertIsInstance(parent.children[0], Consequence)
|
|
73
|
+
|
|
74
|
+
def test_consequence_children_default_not_shared(self):
|
|
75
|
+
"""Each Consequence should have its own children list (no shared mutable default)."""
|
|
76
|
+
from hatch.cli.cli_utils import Consequence, ConsequenceType
|
|
77
|
+
|
|
78
|
+
c1 = Consequence(type=ConsequenceType.CREATE, message="First")
|
|
79
|
+
c2 = Consequence(type=ConsequenceType.CREATE, message="Second")
|
|
80
|
+
|
|
81
|
+
# Modify c1's children
|
|
82
|
+
c1.children.append(Consequence(type=ConsequenceType.UPDATE, message="child"))
|
|
83
|
+
|
|
84
|
+
# c2's children should still be empty
|
|
85
|
+
self.assertEqual(len(c2.children), 0)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestResultReporter(unittest.TestCase):
|
|
89
|
+
"""Tests for ResultReporter state management.
|
|
90
|
+
|
|
91
|
+
Reference: R05 §3.2 - ResultReporter State Management test group
|
|
92
|
+
Reference: R06 §3.4 - ResultReporter interface contract
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def test_result_reporter_exists(self):
|
|
96
|
+
"""ResultReporter class should be importable from cli_utils."""
|
|
97
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
98
|
+
self.assertTrue(callable(ResultReporter))
|
|
99
|
+
|
|
100
|
+
def test_result_reporter_accepts_command_name(self):
|
|
101
|
+
"""ResultReporter should accept command_name argument."""
|
|
102
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
103
|
+
|
|
104
|
+
reporter = ResultReporter(command_name="hatch env create")
|
|
105
|
+
self.assertEqual(reporter.command_name, "hatch env create")
|
|
106
|
+
|
|
107
|
+
def test_result_reporter_command_name_stored(self):
|
|
108
|
+
"""ResultReporter should store command_name correctly."""
|
|
109
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
110
|
+
|
|
111
|
+
reporter = ResultReporter("test-cmd")
|
|
112
|
+
self.assertEqual(reporter.command_name, "test-cmd")
|
|
113
|
+
|
|
114
|
+
def test_result_reporter_dry_run_default_false(self):
|
|
115
|
+
"""ResultReporter dry_run should default to False."""
|
|
116
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
117
|
+
|
|
118
|
+
reporter = ResultReporter("test")
|
|
119
|
+
self.assertFalse(reporter.dry_run)
|
|
120
|
+
|
|
121
|
+
def test_result_reporter_dry_run_stored(self):
|
|
122
|
+
"""ResultReporter should store dry_run flag correctly."""
|
|
123
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
124
|
+
|
|
125
|
+
reporter = ResultReporter("test", dry_run=True)
|
|
126
|
+
self.assertTrue(reporter.dry_run)
|
|
127
|
+
|
|
128
|
+
def test_result_reporter_empty_consequences(self):
|
|
129
|
+
"""Empty reporter should have empty consequences list."""
|
|
130
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
131
|
+
|
|
132
|
+
reporter = ResultReporter("test")
|
|
133
|
+
self.assertEqual(reporter.consequences, [])
|
|
134
|
+
self.assertIsInstance(reporter.consequences, list)
|
|
135
|
+
|
|
136
|
+
def test_result_reporter_add_consequence(self):
|
|
137
|
+
"""ResultReporter.add() should add consequence to list."""
|
|
138
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
139
|
+
|
|
140
|
+
reporter = ResultReporter("test")
|
|
141
|
+
reporter.add(ConsequenceType.CREATE, "Environment 'dev'")
|
|
142
|
+
|
|
143
|
+
self.assertEqual(len(reporter.consequences), 1)
|
|
144
|
+
|
|
145
|
+
def test_result_reporter_consequences_tracked_in_order(self):
|
|
146
|
+
"""Consequences should be tracked in order of add() calls."""
|
|
147
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
148
|
+
|
|
149
|
+
reporter = ResultReporter("test")
|
|
150
|
+
reporter.add(ConsequenceType.CREATE, "First")
|
|
151
|
+
reporter.add(ConsequenceType.REMOVE, "Second")
|
|
152
|
+
reporter.add(ConsequenceType.UPDATE, "Third")
|
|
153
|
+
|
|
154
|
+
self.assertEqual(len(reporter.consequences), 3)
|
|
155
|
+
self.assertEqual(reporter.consequences[0].message, "First")
|
|
156
|
+
self.assertEqual(reporter.consequences[1].message, "Second")
|
|
157
|
+
self.assertEqual(reporter.consequences[2].message, "Third")
|
|
158
|
+
|
|
159
|
+
def test_result_reporter_consequence_data_preserved(self):
|
|
160
|
+
"""Consequence type and message should be preserved."""
|
|
161
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
162
|
+
|
|
163
|
+
reporter = ResultReporter("test")
|
|
164
|
+
reporter.add(ConsequenceType.CONFIGURE, "Server 'weather'")
|
|
165
|
+
|
|
166
|
+
c = reporter.consequences[0]
|
|
167
|
+
self.assertEqual(c.type, ConsequenceType.CONFIGURE)
|
|
168
|
+
self.assertEqual(c.message, "Server 'weather'")
|
|
169
|
+
|
|
170
|
+
def test_result_reporter_add_with_children(self):
|
|
171
|
+
"""ResultReporter.add() should support children argument."""
|
|
172
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType, Consequence
|
|
173
|
+
|
|
174
|
+
reporter = ResultReporter("test")
|
|
175
|
+
children = [
|
|
176
|
+
Consequence(type=ConsequenceType.UPDATE, message="field1"),
|
|
177
|
+
Consequence(type=ConsequenceType.SKIP, message="field2"),
|
|
178
|
+
]
|
|
179
|
+
reporter.add(ConsequenceType.CONFIGURE, "Server", children=children)
|
|
180
|
+
|
|
181
|
+
self.assertEqual(len(reporter.consequences[0].children), 2)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == '__main__':
|
|
185
|
+
unittest.main()
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestConversionReportIntegration(unittest.TestCase):
|
|
189
|
+
"""Tests for ConversionReport → ResultReporter integration.
|
|
190
|
+
|
|
191
|
+
Reference: R05 §3.5 - ConversionReport Integration test group
|
|
192
|
+
Reference: R06 §3.5 - add_from_conversion_report interface
|
|
193
|
+
Reference: R04 §1.2 - field operation → ConsequenceType mapping
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def test_add_from_conversion_report_method_exists(self):
|
|
197
|
+
"""ResultReporter should have add_from_conversion_report method."""
|
|
198
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
199
|
+
|
|
200
|
+
reporter = ResultReporter("test")
|
|
201
|
+
self.assertTrue(hasattr(reporter, 'add_from_conversion_report'))
|
|
202
|
+
self.assertTrue(callable(reporter.add_from_conversion_report))
|
|
203
|
+
|
|
204
|
+
def test_updated_maps_to_update_type(self):
|
|
205
|
+
"""FieldOperation 'UPDATED' should map to ConsequenceType.UPDATE."""
|
|
206
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
207
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_SINGLE_UPDATE
|
|
208
|
+
|
|
209
|
+
reporter = ResultReporter("test")
|
|
210
|
+
reporter.add_from_conversion_report(REPORT_SINGLE_UPDATE)
|
|
211
|
+
|
|
212
|
+
# Should have one resource consequence with one child
|
|
213
|
+
self.assertEqual(len(reporter.consequences), 1)
|
|
214
|
+
self.assertEqual(len(reporter.consequences[0].children), 1)
|
|
215
|
+
self.assertEqual(reporter.consequences[0].children[0].type, ConsequenceType.UPDATE)
|
|
216
|
+
|
|
217
|
+
def test_unsupported_maps_to_skip_type(self):
|
|
218
|
+
"""FieldOperation 'UNSUPPORTED' should map to ConsequenceType.SKIP."""
|
|
219
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
220
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_ALL_UNSUPPORTED
|
|
221
|
+
|
|
222
|
+
reporter = ResultReporter("test")
|
|
223
|
+
reporter.add_from_conversion_report(REPORT_ALL_UNSUPPORTED)
|
|
224
|
+
|
|
225
|
+
# All children should be SKIP type
|
|
226
|
+
for child in reporter.consequences[0].children:
|
|
227
|
+
self.assertEqual(child.type, ConsequenceType.SKIP)
|
|
228
|
+
|
|
229
|
+
def test_unchanged_maps_to_unchanged_type(self):
|
|
230
|
+
"""FieldOperation 'UNCHANGED' should map to ConsequenceType.UNCHANGED."""
|
|
231
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
232
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_ALL_UNCHANGED
|
|
233
|
+
|
|
234
|
+
reporter = ResultReporter("test")
|
|
235
|
+
reporter.add_from_conversion_report(REPORT_ALL_UNCHANGED)
|
|
236
|
+
|
|
237
|
+
# All children should be UNCHANGED type
|
|
238
|
+
for child in reporter.consequences[0].children:
|
|
239
|
+
self.assertEqual(child.type, ConsequenceType.UNCHANGED)
|
|
240
|
+
|
|
241
|
+
def test_field_name_preserved_in_mapping(self):
|
|
242
|
+
"""Field name should be preserved in consequence message."""
|
|
243
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
244
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_SINGLE_UPDATE
|
|
245
|
+
|
|
246
|
+
reporter = ResultReporter("test")
|
|
247
|
+
reporter.add_from_conversion_report(REPORT_SINGLE_UPDATE)
|
|
248
|
+
|
|
249
|
+
child_message = reporter.consequences[0].children[0].message
|
|
250
|
+
self.assertIn("command", child_message)
|
|
251
|
+
|
|
252
|
+
def test_old_new_values_preserved(self):
|
|
253
|
+
"""Old and new values should be preserved in consequence message."""
|
|
254
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
255
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_MIXED_OPERATIONS
|
|
256
|
+
|
|
257
|
+
reporter = ResultReporter("test")
|
|
258
|
+
reporter.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
259
|
+
|
|
260
|
+
# Find the command field child (first one with UPDATED)
|
|
261
|
+
command_child = reporter.consequences[0].children[0]
|
|
262
|
+
self.assertIn("node", command_child.message) # old value
|
|
263
|
+
self.assertIn("python", command_child.message) # new value
|
|
264
|
+
|
|
265
|
+
def test_all_fields_mapped_no_data_loss(self):
|
|
266
|
+
"""All field operations should be mapped (no data loss)."""
|
|
267
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
268
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_MIXED_OPERATIONS
|
|
269
|
+
|
|
270
|
+
reporter = ResultReporter("test")
|
|
271
|
+
reporter.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
272
|
+
|
|
273
|
+
# REPORT_MIXED_OPERATIONS has 4 field operations
|
|
274
|
+
self.assertEqual(len(reporter.consequences[0].children), 4)
|
|
275
|
+
|
|
276
|
+
def test_empty_conversion_report_handled(self):
|
|
277
|
+
"""Empty ConversionReport should not raise exception."""
|
|
278
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
279
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_EMPTY_FIELDS
|
|
280
|
+
|
|
281
|
+
reporter = ResultReporter("test")
|
|
282
|
+
# Should not raise
|
|
283
|
+
reporter.add_from_conversion_report(REPORT_EMPTY_FIELDS)
|
|
284
|
+
|
|
285
|
+
# Should have resource consequence with no children
|
|
286
|
+
self.assertEqual(len(reporter.consequences), 1)
|
|
287
|
+
self.assertEqual(len(reporter.consequences[0].children), 0)
|
|
288
|
+
|
|
289
|
+
def test_resource_consequence_type_from_operation(self):
|
|
290
|
+
"""Resource consequence type should be derived from report.operation."""
|
|
291
|
+
from hatch.cli.cli_utils import ResultReporter, ConsequenceType
|
|
292
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import (
|
|
293
|
+
REPORT_SINGLE_UPDATE, # operation="create"
|
|
294
|
+
REPORT_MIXED_OPERATIONS, # operation="update"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
reporter1 = ResultReporter("test")
|
|
298
|
+
reporter1.add_from_conversion_report(REPORT_SINGLE_UPDATE)
|
|
299
|
+
# "create" operation should map to CONFIGURE (for MCP server creation)
|
|
300
|
+
self.assertIn(
|
|
301
|
+
reporter1.consequences[0].type,
|
|
302
|
+
[ConsequenceType.CONFIGURE, ConsequenceType.CREATE]
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
reporter2 = ResultReporter("test")
|
|
306
|
+
reporter2.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
307
|
+
# "update" operation should map to CONFIGURE or UPDATE
|
|
308
|
+
self.assertIn(
|
|
309
|
+
reporter2.consequences[0].type,
|
|
310
|
+
[ConsequenceType.CONFIGURE, ConsequenceType.UPDATE]
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def test_server_name_in_resource_message(self):
|
|
314
|
+
"""Server name should appear in resource consequence message."""
|
|
315
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
316
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_MIXED_OPERATIONS
|
|
317
|
+
|
|
318
|
+
reporter = ResultReporter("test")
|
|
319
|
+
reporter.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
320
|
+
|
|
321
|
+
self.assertIn("weather-server", reporter.consequences[0].message)
|
|
322
|
+
|
|
323
|
+
def test_target_host_in_resource_message(self):
|
|
324
|
+
"""Target host should appear in resource consequence message."""
|
|
325
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
326
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import REPORT_MIXED_OPERATIONS
|
|
327
|
+
|
|
328
|
+
reporter = ResultReporter("test")
|
|
329
|
+
reporter.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
330
|
+
|
|
331
|
+
self.assertIn("cursor", reporter.consequences[0].message.lower())
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class TestReportError(unittest.TestCase):
|
|
335
|
+
"""Tests for ResultReporter.report_error() method.
|
|
336
|
+
|
|
337
|
+
Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)
|
|
338
|
+
Reference: R13 §7 - Contracts & Invariants
|
|
339
|
+
"""
|
|
340
|
+
|
|
341
|
+
def test_report_error_basic(self):
|
|
342
|
+
"""report_error should print [ERROR] prefix with summary."""
|
|
343
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
344
|
+
import io
|
|
345
|
+
import sys
|
|
346
|
+
|
|
347
|
+
reporter = ResultReporter("test")
|
|
348
|
+
|
|
349
|
+
# Capture stdout
|
|
350
|
+
captured = io.StringIO()
|
|
351
|
+
sys.stdout = captured
|
|
352
|
+
try:
|
|
353
|
+
reporter.report_error("Test error message")
|
|
354
|
+
finally:
|
|
355
|
+
sys.stdout = sys.__stdout__
|
|
356
|
+
|
|
357
|
+
output = captured.getvalue()
|
|
358
|
+
self.assertIn("[ERROR]", output)
|
|
359
|
+
self.assertIn("Test error message", output)
|
|
360
|
+
|
|
361
|
+
def test_report_error_with_details(self):
|
|
362
|
+
"""report_error should print details with indentation."""
|
|
363
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
364
|
+
import io
|
|
365
|
+
import sys
|
|
366
|
+
|
|
367
|
+
reporter = ResultReporter("test")
|
|
368
|
+
|
|
369
|
+
captured = io.StringIO()
|
|
370
|
+
sys.stdout = captured
|
|
371
|
+
try:
|
|
372
|
+
reporter.report_error("Summary", details=["Detail 1", "Detail 2"])
|
|
373
|
+
finally:
|
|
374
|
+
sys.stdout = sys.__stdout__
|
|
375
|
+
|
|
376
|
+
output = captured.getvalue()
|
|
377
|
+
self.assertIn("Detail 1", output)
|
|
378
|
+
self.assertIn("Detail 2", output)
|
|
379
|
+
# Details should be indented (2 spaces)
|
|
380
|
+
self.assertIn(" Detail 1", output)
|
|
381
|
+
|
|
382
|
+
def test_report_error_empty_summary_no_output(self):
|
|
383
|
+
"""report_error with empty summary should produce no output."""
|
|
384
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
385
|
+
import io
|
|
386
|
+
import sys
|
|
387
|
+
|
|
388
|
+
reporter = ResultReporter("test")
|
|
389
|
+
|
|
390
|
+
captured = io.StringIO()
|
|
391
|
+
sys.stdout = captured
|
|
392
|
+
try:
|
|
393
|
+
reporter.report_error("")
|
|
394
|
+
finally:
|
|
395
|
+
sys.stdout = sys.__stdout__
|
|
396
|
+
|
|
397
|
+
output = captured.getvalue()
|
|
398
|
+
self.assertEqual(output, "")
|
|
399
|
+
|
|
400
|
+
def test_report_error_no_color_in_non_tty(self):
|
|
401
|
+
"""report_error should not include ANSI codes when not in TTY."""
|
|
402
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
403
|
+
import io
|
|
404
|
+
import sys
|
|
405
|
+
|
|
406
|
+
reporter = ResultReporter("test")
|
|
407
|
+
|
|
408
|
+
# StringIO is not a TTY, so colors should be disabled
|
|
409
|
+
captured = io.StringIO()
|
|
410
|
+
sys.stdout = captured
|
|
411
|
+
try:
|
|
412
|
+
reporter.report_error("Test error")
|
|
413
|
+
finally:
|
|
414
|
+
sys.stdout = sys.__stdout__
|
|
415
|
+
|
|
416
|
+
output = captured.getvalue()
|
|
417
|
+
# Should not contain ANSI escape codes
|
|
418
|
+
self.assertNotIn("\033[", output)
|
|
419
|
+
|
|
420
|
+
def test_report_error_none_details_handled(self):
|
|
421
|
+
"""report_error should handle None details gracefully."""
|
|
422
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
423
|
+
import io
|
|
424
|
+
import sys
|
|
425
|
+
|
|
426
|
+
reporter = ResultReporter("test")
|
|
427
|
+
|
|
428
|
+
captured = io.StringIO()
|
|
429
|
+
sys.stdout = captured
|
|
430
|
+
try:
|
|
431
|
+
reporter.report_error("Test error", details=None)
|
|
432
|
+
finally:
|
|
433
|
+
sys.stdout = sys.__stdout__
|
|
434
|
+
|
|
435
|
+
output = captured.getvalue()
|
|
436
|
+
self.assertIn("[ERROR]", output)
|
|
437
|
+
self.assertIn("Test error", output)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
class TestReportPartialSuccess(unittest.TestCase):
|
|
441
|
+
"""Tests for ResultReporter.report_partial_success() method.
|
|
442
|
+
|
|
443
|
+
Reference: R13 §4.2.3 (13-error_message_formatting_v0.md)
|
|
444
|
+
Reference: R13 §7 - Contracts & Invariants
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
def test_report_partial_success_basic(self):
|
|
448
|
+
"""report_partial_success should print [WARNING] prefix."""
|
|
449
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
450
|
+
import io
|
|
451
|
+
import sys
|
|
452
|
+
|
|
453
|
+
reporter = ResultReporter("test")
|
|
454
|
+
|
|
455
|
+
captured = io.StringIO()
|
|
456
|
+
sys.stdout = captured
|
|
457
|
+
try:
|
|
458
|
+
reporter.report_partial_success("Test summary", ["ok"], [("fail", "reason")])
|
|
459
|
+
finally:
|
|
460
|
+
sys.stdout = sys.__stdout__
|
|
461
|
+
|
|
462
|
+
output = captured.getvalue()
|
|
463
|
+
self.assertIn("[WARNING]", output)
|
|
464
|
+
self.assertIn("Test summary", output)
|
|
465
|
+
|
|
466
|
+
def test_report_partial_success_unicode_symbols(self):
|
|
467
|
+
"""report_partial_success should use ✓/✗ symbols in UTF-8 terminals."""
|
|
468
|
+
from hatch.cli.cli_utils import ResultReporter, _supports_unicode
|
|
469
|
+
import io
|
|
470
|
+
import sys
|
|
471
|
+
|
|
472
|
+
reporter = ResultReporter("test")
|
|
473
|
+
|
|
474
|
+
captured = io.StringIO()
|
|
475
|
+
sys.stdout = captured
|
|
476
|
+
try:
|
|
477
|
+
reporter.report_partial_success("Test", ["success"], [("fail", "reason")])
|
|
478
|
+
finally:
|
|
479
|
+
sys.stdout = sys.__stdout__
|
|
480
|
+
|
|
481
|
+
output = captured.getvalue()
|
|
482
|
+
if _supports_unicode():
|
|
483
|
+
self.assertIn("✓", output)
|
|
484
|
+
self.assertIn("✗", output)
|
|
485
|
+
else:
|
|
486
|
+
self.assertIn("+", output)
|
|
487
|
+
self.assertIn("x", output)
|
|
488
|
+
|
|
489
|
+
def test_report_partial_success_ascii_fallback(self):
|
|
490
|
+
"""report_partial_success should use +/x in non-UTF8 terminals."""
|
|
491
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
492
|
+
import io
|
|
493
|
+
import sys
|
|
494
|
+
import unittest.mock as mock
|
|
495
|
+
|
|
496
|
+
reporter = ResultReporter("test")
|
|
497
|
+
|
|
498
|
+
captured = io.StringIO()
|
|
499
|
+
sys.stdout = captured
|
|
500
|
+
try:
|
|
501
|
+
# Mock _supports_unicode to return False
|
|
502
|
+
with mock.patch('hatch.cli.cli_utils._supports_unicode', return_value=False):
|
|
503
|
+
reporter.report_partial_success("Test", ["success"], [("fail", "reason")])
|
|
504
|
+
finally:
|
|
505
|
+
sys.stdout = sys.__stdout__
|
|
506
|
+
|
|
507
|
+
output = captured.getvalue()
|
|
508
|
+
self.assertIn("+", output)
|
|
509
|
+
self.assertIn("x", output)
|
|
510
|
+
|
|
511
|
+
def test_report_partial_success_summary_line(self):
|
|
512
|
+
"""report_partial_success should include summary line with counts."""
|
|
513
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
514
|
+
import io
|
|
515
|
+
import sys
|
|
516
|
+
|
|
517
|
+
reporter = ResultReporter("test")
|
|
518
|
+
|
|
519
|
+
captured = io.StringIO()
|
|
520
|
+
sys.stdout = captured
|
|
521
|
+
try:
|
|
522
|
+
reporter.report_partial_success(
|
|
523
|
+
"Test",
|
|
524
|
+
["ok1", "ok2"],
|
|
525
|
+
[("fail1", "r1"), ("fail2", "r2"), ("fail3", "r3")]
|
|
526
|
+
)
|
|
527
|
+
finally:
|
|
528
|
+
sys.stdout = sys.__stdout__
|
|
529
|
+
|
|
530
|
+
output = captured.getvalue()
|
|
531
|
+
self.assertIn("Summary: 2/5 succeeded", output)
|
|
532
|
+
|
|
533
|
+
def test_report_partial_success_no_color_in_non_tty(self):
|
|
534
|
+
"""report_partial_success should not include ANSI codes when not in TTY."""
|
|
535
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
536
|
+
import io
|
|
537
|
+
import sys
|
|
538
|
+
|
|
539
|
+
reporter = ResultReporter("test")
|
|
540
|
+
|
|
541
|
+
captured = io.StringIO()
|
|
542
|
+
sys.stdout = captured
|
|
543
|
+
try:
|
|
544
|
+
reporter.report_partial_success("Test", ["ok"], [("fail", "reason")])
|
|
545
|
+
finally:
|
|
546
|
+
sys.stdout = sys.__stdout__
|
|
547
|
+
|
|
548
|
+
output = captured.getvalue()
|
|
549
|
+
self.assertNotIn("\033[", output)
|
|
550
|
+
|
|
551
|
+
def test_report_partial_success_failure_reason_shown(self):
|
|
552
|
+
"""report_partial_success should show failure reason after colon."""
|
|
553
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
554
|
+
import io
|
|
555
|
+
import sys
|
|
556
|
+
|
|
557
|
+
reporter = ResultReporter("test")
|
|
558
|
+
|
|
559
|
+
captured = io.StringIO()
|
|
560
|
+
sys.stdout = captured
|
|
561
|
+
try:
|
|
562
|
+
reporter.report_partial_success("Test", [], [("cursor", "Config file not found")])
|
|
563
|
+
finally:
|
|
564
|
+
sys.stdout = sys.__stdout__
|
|
565
|
+
|
|
566
|
+
output = captured.getvalue()
|
|
567
|
+
self.assertIn("cursor: Config file not found", output)
|
|
568
|
+
|
|
569
|
+
def test_report_partial_success_empty_lists(self):
|
|
570
|
+
"""report_partial_success should handle empty success/failure lists."""
|
|
571
|
+
from hatch.cli.cli_utils import ResultReporter
|
|
572
|
+
import io
|
|
573
|
+
import sys
|
|
574
|
+
|
|
575
|
+
reporter = ResultReporter("test")
|
|
576
|
+
|
|
577
|
+
captured = io.StringIO()
|
|
578
|
+
sys.stdout = captured
|
|
579
|
+
try:
|
|
580
|
+
reporter.report_partial_success("Test", [], [])
|
|
581
|
+
finally:
|
|
582
|
+
sys.stdout = sys.__stdout__
|
|
583
|
+
|
|
584
|
+
output = captured.getvalue()
|
|
585
|
+
self.assertIn("[WARNING]", output)
|
|
586
|
+
self.assertIn("Summary: 0/0 succeeded", output)
|