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,211 @@
|
|
|
1
|
+
"""Regression tests for TableFormatter class.
|
|
2
|
+
|
|
3
|
+
Tests focus on behavioral contracts for table rendering:
|
|
4
|
+
- Column alignment (left, right, center)
|
|
5
|
+
- Auto-width calculation
|
|
6
|
+
- Header and separator rendering
|
|
7
|
+
- Row data handling
|
|
8
|
+
|
|
9
|
+
Reference: R02 §5 (02-list_output_format_specification_v2.md)
|
|
10
|
+
Reference: R06 §3.6 (06-dependency_analysis_v0.md)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestColumnDef:
|
|
17
|
+
"""Tests for ColumnDef dataclass."""
|
|
18
|
+
|
|
19
|
+
def test_column_def_has_required_fields(self):
|
|
20
|
+
"""ColumnDef must have name, width, and align fields."""
|
|
21
|
+
from hatch.cli.cli_utils import ColumnDef
|
|
22
|
+
|
|
23
|
+
col = ColumnDef(name="Test", width=10)
|
|
24
|
+
assert col.name == "Test"
|
|
25
|
+
assert col.width == 10
|
|
26
|
+
assert col.align == "left" # Default alignment
|
|
27
|
+
|
|
28
|
+
def test_column_def_accepts_auto_width(self):
|
|
29
|
+
"""ColumnDef width can be 'auto' for auto-calculation."""
|
|
30
|
+
from hatch.cli.cli_utils import ColumnDef
|
|
31
|
+
|
|
32
|
+
col = ColumnDef(name="Test", width="auto")
|
|
33
|
+
assert col.width == "auto"
|
|
34
|
+
|
|
35
|
+
def test_column_def_accepts_alignment_options(self):
|
|
36
|
+
"""ColumnDef supports left, right, and center alignment."""
|
|
37
|
+
from hatch.cli.cli_utils import ColumnDef
|
|
38
|
+
|
|
39
|
+
left = ColumnDef(name="Left", width=10, align="left")
|
|
40
|
+
right = ColumnDef(name="Right", width=10, align="right")
|
|
41
|
+
center = ColumnDef(name="Center", width=10, align="center")
|
|
42
|
+
|
|
43
|
+
assert left.align == "left"
|
|
44
|
+
assert right.align == "right"
|
|
45
|
+
assert center.align == "center"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestTableFormatter:
|
|
49
|
+
"""Tests for TableFormatter class."""
|
|
50
|
+
|
|
51
|
+
def test_table_formatter_accepts_column_definitions(self):
|
|
52
|
+
"""TableFormatter initializes with column definitions."""
|
|
53
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
54
|
+
|
|
55
|
+
columns = [
|
|
56
|
+
ColumnDef(name="Name", width=20),
|
|
57
|
+
ColumnDef(name="Value", width=10),
|
|
58
|
+
]
|
|
59
|
+
formatter = TableFormatter(columns)
|
|
60
|
+
assert formatter is not None
|
|
61
|
+
|
|
62
|
+
def test_add_row_stores_data(self):
|
|
63
|
+
"""add_row stores row data for rendering."""
|
|
64
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
65
|
+
|
|
66
|
+
columns = [ColumnDef(name="Col1", width=10)]
|
|
67
|
+
formatter = TableFormatter(columns)
|
|
68
|
+
formatter.add_row(["value1"])
|
|
69
|
+
formatter.add_row(["value2"])
|
|
70
|
+
|
|
71
|
+
# Verify rows are stored (implementation detail, but necessary for render)
|
|
72
|
+
assert len(formatter._rows) == 2
|
|
73
|
+
|
|
74
|
+
def test_render_produces_string_output(self):
|
|
75
|
+
"""render() returns a string with table content."""
|
|
76
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
77
|
+
|
|
78
|
+
columns = [ColumnDef(name="Name", width=10)]
|
|
79
|
+
formatter = TableFormatter(columns)
|
|
80
|
+
formatter.add_row(["Test"])
|
|
81
|
+
|
|
82
|
+
output = formatter.render()
|
|
83
|
+
assert isinstance(output, str)
|
|
84
|
+
assert len(output) > 0
|
|
85
|
+
|
|
86
|
+
def test_render_includes_header_row(self):
|
|
87
|
+
"""Rendered output includes column headers."""
|
|
88
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
89
|
+
|
|
90
|
+
columns = [
|
|
91
|
+
ColumnDef(name="Name", width=15),
|
|
92
|
+
ColumnDef(name="Status", width=10),
|
|
93
|
+
]
|
|
94
|
+
formatter = TableFormatter(columns)
|
|
95
|
+
formatter.add_row(["test-item", "active"])
|
|
96
|
+
|
|
97
|
+
output = formatter.render()
|
|
98
|
+
assert "Name" in output
|
|
99
|
+
assert "Status" in output
|
|
100
|
+
|
|
101
|
+
def test_render_includes_separator_line(self):
|
|
102
|
+
"""Rendered output includes separator line after headers."""
|
|
103
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
104
|
+
|
|
105
|
+
columns = [ColumnDef(name="Name", width=10)]
|
|
106
|
+
formatter = TableFormatter(columns)
|
|
107
|
+
formatter.add_row(["Test"])
|
|
108
|
+
|
|
109
|
+
output = formatter.render()
|
|
110
|
+
# Separator uses box-drawing character or dashes
|
|
111
|
+
assert "─" in output or "-" in output
|
|
112
|
+
|
|
113
|
+
def test_render_includes_data_rows(self):
|
|
114
|
+
"""Rendered output includes all added data rows."""
|
|
115
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
116
|
+
|
|
117
|
+
columns = [ColumnDef(name="Item", width=15)]
|
|
118
|
+
formatter = TableFormatter(columns)
|
|
119
|
+
formatter.add_row(["first-item"])
|
|
120
|
+
formatter.add_row(["second-item"])
|
|
121
|
+
formatter.add_row(["third-item"])
|
|
122
|
+
|
|
123
|
+
output = formatter.render()
|
|
124
|
+
assert "first-item" in output
|
|
125
|
+
assert "second-item" in output
|
|
126
|
+
assert "third-item" in output
|
|
127
|
+
|
|
128
|
+
def test_left_alignment_pads_right(self):
|
|
129
|
+
"""Left-aligned columns pad values on the right."""
|
|
130
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
131
|
+
|
|
132
|
+
columns = [ColumnDef(name="Name", width=10, align="left")]
|
|
133
|
+
formatter = TableFormatter(columns)
|
|
134
|
+
formatter.add_row(["abc"])
|
|
135
|
+
|
|
136
|
+
output = formatter.render()
|
|
137
|
+
lines = output.strip().split("\n")
|
|
138
|
+
# Find data row (skip header and separator)
|
|
139
|
+
data_line = lines[-1]
|
|
140
|
+
# Left-aligned: value followed by spaces
|
|
141
|
+
assert "abc" in data_line
|
|
142
|
+
|
|
143
|
+
def test_right_alignment_pads_left(self):
|
|
144
|
+
"""Right-aligned columns pad values on the left."""
|
|
145
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
146
|
+
|
|
147
|
+
columns = [ColumnDef(name="Count", width=10, align="right")]
|
|
148
|
+
formatter = TableFormatter(columns)
|
|
149
|
+
formatter.add_row(["42"])
|
|
150
|
+
|
|
151
|
+
output = formatter.render()
|
|
152
|
+
lines = output.strip().split("\n")
|
|
153
|
+
data_line = lines[-1]
|
|
154
|
+
# Right-aligned: spaces followed by value
|
|
155
|
+
assert "42" in data_line
|
|
156
|
+
|
|
157
|
+
def test_auto_width_calculates_from_content(self):
|
|
158
|
+
"""Auto width calculates based on header and data content."""
|
|
159
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
160
|
+
|
|
161
|
+
columns = [ColumnDef(name="Name", width="auto")]
|
|
162
|
+
formatter = TableFormatter(columns)
|
|
163
|
+
formatter.add_row(["short"])
|
|
164
|
+
formatter.add_row(["much-longer-value"])
|
|
165
|
+
|
|
166
|
+
output = formatter.render()
|
|
167
|
+
# Output should accommodate the longest value
|
|
168
|
+
assert "much-longer-value" in output
|
|
169
|
+
|
|
170
|
+
def test_empty_table_renders_headers_only(self):
|
|
171
|
+
"""Table with no rows renders headers and separator."""
|
|
172
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
173
|
+
|
|
174
|
+
columns = [ColumnDef(name="Empty", width=10)]
|
|
175
|
+
formatter = TableFormatter(columns)
|
|
176
|
+
|
|
177
|
+
output = formatter.render()
|
|
178
|
+
assert "Empty" in output
|
|
179
|
+
# Should have header and separator, but no data rows
|
|
180
|
+
|
|
181
|
+
def test_multiple_columns_separated(self):
|
|
182
|
+
"""Multiple columns are visually separated."""
|
|
183
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
184
|
+
|
|
185
|
+
columns = [
|
|
186
|
+
ColumnDef(name="Col1", width=10),
|
|
187
|
+
ColumnDef(name="Col2", width=10),
|
|
188
|
+
ColumnDef(name="Col3", width=10),
|
|
189
|
+
]
|
|
190
|
+
formatter = TableFormatter(columns)
|
|
191
|
+
formatter.add_row(["a", "b", "c"])
|
|
192
|
+
|
|
193
|
+
output = formatter.render()
|
|
194
|
+
assert "Col1" in output
|
|
195
|
+
assert "Col2" in output
|
|
196
|
+
assert "Col3" in output
|
|
197
|
+
assert "a" in output
|
|
198
|
+
assert "b" in output
|
|
199
|
+
assert "c" in output
|
|
200
|
+
|
|
201
|
+
def test_truncation_with_ellipsis(self):
|
|
202
|
+
"""Values exceeding column width are truncated with ellipsis."""
|
|
203
|
+
from hatch.cli.cli_utils import TableFormatter, ColumnDef
|
|
204
|
+
|
|
205
|
+
columns = [ColumnDef(name="Name", width=8)]
|
|
206
|
+
formatter = TableFormatter(columns)
|
|
207
|
+
formatter.add_row(["very-long-value-that-exceeds-width"])
|
|
208
|
+
|
|
209
|
+
output = formatter.render()
|
|
210
|
+
# Should truncate and add ellipsis
|
|
211
|
+
assert "…" in output or "..." in output
|
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Regression tests for field filtering (name/type exclusion).
|
|
2
|
+
|
|
3
|
+
Test IDs: RF-01 to RF-07 (per 02-test_architecture_rebuild_v0.md)
|
|
4
|
+
Scope: Prevent `name` and `type` field leakage in serialized output.
|
|
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
|
+
VSCodeAdapter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestFieldFiltering(unittest.TestCase):
|
|
21
|
+
"""Regression tests for field filtering (RF-01 to RF-07).
|
|
22
|
+
|
|
23
|
+
These tests ensure:
|
|
24
|
+
- `name` is NEVER in serialized output (it's Hatch metadata, not host config)
|
|
25
|
+
- `type` behavior varies by host (some include, some exclude)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def setUp(self):
|
|
29
|
+
"""Create test configs for use across tests."""
|
|
30
|
+
# Config WITH type (for hosts that support type field)
|
|
31
|
+
self.stdio_config_with_type = MCPServerConfig(
|
|
32
|
+
name="test-server",
|
|
33
|
+
command="python",
|
|
34
|
+
args=["server.py"],
|
|
35
|
+
type="stdio",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Config WITHOUT type (for hosts that don't support type field)
|
|
39
|
+
self.stdio_config_no_type = MCPServerConfig(
|
|
40
|
+
name="test-server",
|
|
41
|
+
command="python",
|
|
42
|
+
args=["server.py"],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.sse_config_with_type = MCPServerConfig(
|
|
46
|
+
name="sse-server",
|
|
47
|
+
url="https://example.com/mcp",
|
|
48
|
+
type="sse",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
self.sse_config_no_type = MCPServerConfig(
|
|
52
|
+
name="sse-server",
|
|
53
|
+
url="https://example.com/mcp",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def test_RF01_name_never_in_gemini_output(self):
|
|
57
|
+
"""RF-01: `name` never appears in Gemini serialized output."""
|
|
58
|
+
adapter = GeminiAdapter()
|
|
59
|
+
result = adapter.serialize(self.stdio_config_no_type)
|
|
60
|
+
|
|
61
|
+
self.assertNotIn("name", result)
|
|
62
|
+
|
|
63
|
+
def test_RF02_name_never_in_claude_output(self):
|
|
64
|
+
"""RF-02: `name` never appears in Claude serialized output."""
|
|
65
|
+
adapter = ClaudeAdapter()
|
|
66
|
+
result = adapter.serialize(self.stdio_config_with_type)
|
|
67
|
+
|
|
68
|
+
self.assertNotIn("name", result)
|
|
69
|
+
|
|
70
|
+
def test_RF03_type_not_in_gemini_output(self):
|
|
71
|
+
"""RF-03: `type` should NOT be in Gemini output.
|
|
72
|
+
|
|
73
|
+
Gemini's config format infers type from the presence of
|
|
74
|
+
command/url/httpUrl fields.
|
|
75
|
+
"""
|
|
76
|
+
adapter = GeminiAdapter()
|
|
77
|
+
result = adapter.serialize(self.stdio_config_no_type)
|
|
78
|
+
|
|
79
|
+
self.assertNotIn("type", result)
|
|
80
|
+
|
|
81
|
+
def test_RF04_type_not_in_kiro_output(self):
|
|
82
|
+
"""RF-04: `type` should NOT be in Kiro output.
|
|
83
|
+
|
|
84
|
+
Kiro's config format infers type from the presence of
|
|
85
|
+
command/url fields.
|
|
86
|
+
"""
|
|
87
|
+
adapter = KiroAdapter()
|
|
88
|
+
result = adapter.serialize(self.stdio_config_no_type)
|
|
89
|
+
|
|
90
|
+
self.assertNotIn("type", result)
|
|
91
|
+
|
|
92
|
+
def test_RF05_type_not_in_codex_output(self):
|
|
93
|
+
"""RF-05: `type` should NOT be in Codex output.
|
|
94
|
+
|
|
95
|
+
Codex TOML format doesn't use type field - it uses section headers.
|
|
96
|
+
"""
|
|
97
|
+
adapter = CodexAdapter()
|
|
98
|
+
result = adapter.serialize(self.stdio_config_no_type)
|
|
99
|
+
|
|
100
|
+
self.assertNotIn("type", result)
|
|
101
|
+
|
|
102
|
+
def test_RF06_type_IS_in_claude_output(self):
|
|
103
|
+
"""RF-06: `type` SHOULD be in Claude output.
|
|
104
|
+
|
|
105
|
+
Claude Desktop/Code explicitly uses the type field for transport.
|
|
106
|
+
"""
|
|
107
|
+
adapter = ClaudeAdapter()
|
|
108
|
+
result = adapter.serialize(self.stdio_config_with_type)
|
|
109
|
+
|
|
110
|
+
self.assertIn("type", result)
|
|
111
|
+
self.assertEqual(result["type"], "stdio")
|
|
112
|
+
|
|
113
|
+
def test_RF07_type_IS_in_vscode_output(self):
|
|
114
|
+
"""RF-07: `type` SHOULD be in VS Code output.
|
|
115
|
+
|
|
116
|
+
VS Code explicitly uses the type field for transport.
|
|
117
|
+
"""
|
|
118
|
+
adapter = VSCodeAdapter()
|
|
119
|
+
result = adapter.serialize(self.stdio_config_with_type)
|
|
120
|
+
|
|
121
|
+
self.assertIn("type", result)
|
|
122
|
+
self.assertEqual(result["type"], "stdio")
|
|
123
|
+
|
|
124
|
+
def test_name_never_in_any_adapter_output(self):
|
|
125
|
+
"""Comprehensive test: `name` never appears in ANY adapter output.
|
|
126
|
+
|
|
127
|
+
Uses appropriate config for each adapter (with/without type field).
|
|
128
|
+
"""
|
|
129
|
+
type_supporting_adapters = [
|
|
130
|
+
ClaudeAdapter(),
|
|
131
|
+
CursorAdapter(),
|
|
132
|
+
VSCodeAdapter(),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
type_rejecting_adapters = [
|
|
136
|
+
CodexAdapter(),
|
|
137
|
+
GeminiAdapter(),
|
|
138
|
+
KiroAdapter(),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
for adapter in type_supporting_adapters:
|
|
142
|
+
with self.subTest(adapter=adapter.host_name):
|
|
143
|
+
result = adapter.serialize(self.stdio_config_with_type)
|
|
144
|
+
self.assertNotIn("name", result)
|
|
145
|
+
|
|
146
|
+
for adapter in type_rejecting_adapters:
|
|
147
|
+
with self.subTest(adapter=adapter.host_name):
|
|
148
|
+
result = adapter.serialize(self.stdio_config_no_type)
|
|
149
|
+
self.assertNotIn("name", result)
|
|
150
|
+
|
|
151
|
+
def test_cursor_type_behavior(self):
|
|
152
|
+
"""Test Cursor type field behavior (same as VS Code)."""
|
|
153
|
+
adapter = CursorAdapter()
|
|
154
|
+
result = adapter.serialize(self.stdio_config_with_type)
|
|
155
|
+
|
|
156
|
+
# Cursor should include type like VS Code
|
|
157
|
+
self.assertIn("type", result)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
unittest.main()
|
|
162
|
+
|
tests/test_cli_version.py
CHANGED
|
@@ -20,7 +20,8 @@ from io import StringIO
|
|
|
20
20
|
# Add parent directory to path
|
|
21
21
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
22
22
|
|
|
23
|
-
from hatch.cli_hatch import main
|
|
23
|
+
from hatch.cli_hatch import main
|
|
24
|
+
from hatch.cli.cli_utils import get_hatch_version
|
|
24
25
|
|
|
25
26
|
try:
|
|
26
27
|
from wobble.decorators import regression_test, integration_test
|
|
@@ -41,7 +42,7 @@ class TestVersionCommand(unittest.TestCase):
|
|
|
41
42
|
@regression_test
|
|
42
43
|
def test_get_hatch_version_retrieves_from_metadata(self):
|
|
43
44
|
"""Test get_hatch_version() retrieves version from importlib.metadata."""
|
|
44
|
-
with patch('hatch.
|
|
45
|
+
with patch('hatch.cli.cli_utils.version', return_value='0.7.0-dev.3') as mock_version:
|
|
45
46
|
result = get_hatch_version()
|
|
46
47
|
|
|
47
48
|
self.assertEqual(result, '0.7.0-dev.3')
|
|
@@ -52,7 +53,7 @@ class TestVersionCommand(unittest.TestCase):
|
|
|
52
53
|
"""Test get_hatch_version() handles PackageNotFoundError gracefully."""
|
|
53
54
|
from importlib.metadata import PackageNotFoundError
|
|
54
55
|
|
|
55
|
-
with patch('hatch.
|
|
56
|
+
with patch('hatch.cli.cli_utils.version', side_effect=PackageNotFoundError()):
|
|
56
57
|
result = get_hatch_version()
|
|
57
58
|
|
|
58
59
|
self.assertEqual(result, 'unknown (development mode)')
|
|
@@ -63,7 +64,8 @@ class TestVersionCommand(unittest.TestCase):
|
|
|
63
64
|
test_args = ['hatch', '--version']
|
|
64
65
|
|
|
65
66
|
with patch('sys.argv', test_args):
|
|
66
|
-
|
|
67
|
+
# Patch at point of use in __main__ (imported from cli_utils)
|
|
68
|
+
with patch('hatch.cli.__main__.get_hatch_version', return_value='0.7.0-dev.3'):
|
|
67
69
|
with patch('sys.stdout', new_callable=StringIO) as mock_stdout:
|
|
68
70
|
with self.assertRaises(SystemExit) as cm:
|
|
69
71
|
main()
|
|
@@ -98,7 +100,7 @@ class TestVersionCommand(unittest.TestCase):
|
|
|
98
100
|
test_args = ['hatch', 'package', 'add', 'test-package', '-v', '1.0.0']
|
|
99
101
|
|
|
100
102
|
with patch('sys.argv', test_args):
|
|
101
|
-
with patch('hatch.
|
|
103
|
+
with patch('hatch.environment_manager.HatchEnvironmentManager') as mock_env:
|
|
102
104
|
mock_env_instance = MagicMock()
|
|
103
105
|
mock_env.return_value = mock_env_instance
|
|
104
106
|
mock_env_instance.add_package_to_environment.return_value = True
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Test fixtures for ResultReporter and ConversionReport integration tests.
|
|
2
|
+
|
|
3
|
+
This module provides reusable ConversionReport and FieldOperation samples
|
|
4
|
+
for testing the CLI reporter infrastructure. Fixtures are defined as Python
|
|
5
|
+
objects for type safety and IDE support.
|
|
6
|
+
|
|
7
|
+
Reference: R05 §4.2 (05-test_definition_v0.md)
|
|
8
|
+
|
|
9
|
+
Fixture Categories:
|
|
10
|
+
- Single field operation samples (one per operation type)
|
|
11
|
+
- ConversionReport samples (various scenarios)
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from tests.test_data.fixtures.cli_reporter_fixtures import (
|
|
15
|
+
REPORT_MIXED_OPERATIONS,
|
|
16
|
+
FIELD_OP_UPDATED,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def test_all_fields_mapped_no_data_loss(self):
|
|
20
|
+
reporter = ResultReporter("test")
|
|
21
|
+
reporter.add_from_conversion_report(REPORT_MIXED_OPERATIONS)
|
|
22
|
+
assert len(reporter.consequences[0].children) == 4
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from hatch.mcp_host_config.reporting import ConversionReport, FieldOperation
|
|
26
|
+
from hatch.mcp_host_config.models import MCPHostType
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# =============================================================================
|
|
30
|
+
# Single Field Operation Samples (one per operation type)
|
|
31
|
+
# =============================================================================
|
|
32
|
+
|
|
33
|
+
FIELD_OP_UPDATED = FieldOperation(
|
|
34
|
+
field_name="command",
|
|
35
|
+
operation="UPDATED",
|
|
36
|
+
old_value=None,
|
|
37
|
+
new_value="python"
|
|
38
|
+
)
|
|
39
|
+
"""Field operation: UPDATED - field value changed from None to 'python'."""
|
|
40
|
+
|
|
41
|
+
FIELD_OP_UPDATED_WITH_OLD = FieldOperation(
|
|
42
|
+
field_name="command",
|
|
43
|
+
operation="UPDATED",
|
|
44
|
+
old_value="node",
|
|
45
|
+
new_value="python"
|
|
46
|
+
)
|
|
47
|
+
"""Field operation: UPDATED - field value changed from 'node' to 'python'."""
|
|
48
|
+
|
|
49
|
+
FIELD_OP_UNSUPPORTED = FieldOperation(
|
|
50
|
+
field_name="timeout",
|
|
51
|
+
operation="UNSUPPORTED",
|
|
52
|
+
new_value=30
|
|
53
|
+
)
|
|
54
|
+
"""Field operation: UNSUPPORTED - field not supported by target host."""
|
|
55
|
+
|
|
56
|
+
FIELD_OP_UNCHANGED = FieldOperation(
|
|
57
|
+
field_name="env",
|
|
58
|
+
operation="UNCHANGED",
|
|
59
|
+
new_value={}
|
|
60
|
+
)
|
|
61
|
+
"""Field operation: UNCHANGED - field value remained the same."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# ConversionReport Samples
|
|
66
|
+
# =============================================================================
|
|
67
|
+
|
|
68
|
+
REPORT_SINGLE_UPDATE = ConversionReport(
|
|
69
|
+
operation="create",
|
|
70
|
+
server_name="test-server",
|
|
71
|
+
target_host=MCPHostType.CLAUDE_DESKTOP,
|
|
72
|
+
field_operations=[FIELD_OP_UPDATED]
|
|
73
|
+
)
|
|
74
|
+
"""ConversionReport: Single field update (create operation)."""
|
|
75
|
+
|
|
76
|
+
REPORT_MIXED_OPERATIONS = ConversionReport(
|
|
77
|
+
operation="update",
|
|
78
|
+
server_name="weather-server",
|
|
79
|
+
target_host=MCPHostType.CURSOR,
|
|
80
|
+
field_operations=[
|
|
81
|
+
FieldOperation(
|
|
82
|
+
field_name="command",
|
|
83
|
+
operation="UPDATED",
|
|
84
|
+
old_value="node",
|
|
85
|
+
new_value="python"
|
|
86
|
+
),
|
|
87
|
+
FieldOperation(
|
|
88
|
+
field_name="args",
|
|
89
|
+
operation="UPDATED",
|
|
90
|
+
old_value=[],
|
|
91
|
+
new_value=["server.py"]
|
|
92
|
+
),
|
|
93
|
+
FieldOperation(
|
|
94
|
+
field_name="env",
|
|
95
|
+
operation="UNCHANGED",
|
|
96
|
+
new_value={"API_KEY": "***"}
|
|
97
|
+
),
|
|
98
|
+
FieldOperation(
|
|
99
|
+
field_name="timeout",
|
|
100
|
+
operation="UNSUPPORTED",
|
|
101
|
+
new_value=60
|
|
102
|
+
),
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
"""ConversionReport: Mixed field operations (update operation).
|
|
106
|
+
|
|
107
|
+
Contains:
|
|
108
|
+
- 2 UPDATED fields (command, args)
|
|
109
|
+
- 1 UNCHANGED field (env)
|
|
110
|
+
- 1 UNSUPPORTED field (timeout)
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
REPORT_EMPTY_FIELDS = ConversionReport(
|
|
114
|
+
operation="create",
|
|
115
|
+
server_name="minimal-server",
|
|
116
|
+
target_host=MCPHostType.VSCODE,
|
|
117
|
+
field_operations=[]
|
|
118
|
+
)
|
|
119
|
+
"""ConversionReport: Empty field operations list (edge case)."""
|
|
120
|
+
|
|
121
|
+
REPORT_ALL_UNSUPPORTED = ConversionReport(
|
|
122
|
+
operation="migrate",
|
|
123
|
+
server_name="legacy-server",
|
|
124
|
+
source_host=MCPHostType.CLAUDE_DESKTOP,
|
|
125
|
+
target_host=MCPHostType.KIRO,
|
|
126
|
+
field_operations=[
|
|
127
|
+
FieldOperation(
|
|
128
|
+
field_name="trust",
|
|
129
|
+
operation="UNSUPPORTED",
|
|
130
|
+
new_value=True
|
|
131
|
+
),
|
|
132
|
+
FieldOperation(
|
|
133
|
+
field_name="cwd",
|
|
134
|
+
operation="UNSUPPORTED",
|
|
135
|
+
new_value="/app"
|
|
136
|
+
),
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
"""ConversionReport: All fields unsupported (migrate operation)."""
|
|
140
|
+
|
|
141
|
+
REPORT_ALL_UNCHANGED = ConversionReport(
|
|
142
|
+
operation="update",
|
|
143
|
+
server_name="stable-server",
|
|
144
|
+
target_host=MCPHostType.CLAUDE_DESKTOP,
|
|
145
|
+
field_operations=[
|
|
146
|
+
FieldOperation(
|
|
147
|
+
field_name="command",
|
|
148
|
+
operation="UNCHANGED",
|
|
149
|
+
new_value="python"
|
|
150
|
+
),
|
|
151
|
+
FieldOperation(
|
|
152
|
+
field_name="args",
|
|
153
|
+
operation="UNCHANGED",
|
|
154
|
+
new_value=["server.py"]
|
|
155
|
+
),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
"""ConversionReport: All fields unchanged (no-op update)."""
|
|
159
|
+
|
|
160
|
+
REPORT_DRY_RUN = ConversionReport(
|
|
161
|
+
operation="create",
|
|
162
|
+
server_name="preview-server",
|
|
163
|
+
target_host=MCPHostType.CURSOR,
|
|
164
|
+
field_operations=[
|
|
165
|
+
FieldOperation(
|
|
166
|
+
field_name="command",
|
|
167
|
+
operation="UPDATED",
|
|
168
|
+
old_value=None,
|
|
169
|
+
new_value="python"
|
|
170
|
+
),
|
|
171
|
+
],
|
|
172
|
+
dry_run=True
|
|
173
|
+
)
|
|
174
|
+
"""ConversionReport: Dry-run mode enabled."""
|
|
175
|
+
|
|
176
|
+
REPORT_WITH_ERROR = ConversionReport(
|
|
177
|
+
operation="create",
|
|
178
|
+
server_name="failed-server",
|
|
179
|
+
target_host=MCPHostType.VSCODE,
|
|
180
|
+
success=False,
|
|
181
|
+
error_message="Configuration file not found",
|
|
182
|
+
field_operations=[]
|
|
183
|
+
)
|
|
184
|
+
"""ConversionReport: Failed operation with error message."""
|
tests/unit/__init__.py
ADDED
|
File without changes
|
|
File without changes
|