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
|
@@ -1,603 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Test suite for Round 04 v4 Pydantic Model Hierarchy.
|
|
3
|
-
|
|
4
|
-
This module tests the new model hierarchy including MCPServerConfigBase,
|
|
5
|
-
host-specific models (Gemini, VS Code, Cursor, Claude), MCPServerConfigOmni,
|
|
6
|
-
HOST_MODEL_REGISTRY, and from_omni() conversion methods.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import unittest
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
# Add the parent directory to the path to import wobble
|
|
14
|
-
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
from wobble.decorators import regression_test
|
|
18
|
-
except ImportError:
|
|
19
|
-
# Fallback decorator if wobble is not available
|
|
20
|
-
def regression_test(func):
|
|
21
|
-
return func
|
|
22
|
-
|
|
23
|
-
from hatch.mcp_host_config.models import (
|
|
24
|
-
MCPServerConfigBase,
|
|
25
|
-
MCPServerConfigGemini,
|
|
26
|
-
MCPServerConfigVSCode,
|
|
27
|
-
MCPServerConfigCursor,
|
|
28
|
-
MCPServerConfigClaude,
|
|
29
|
-
MCPServerConfigOmni,
|
|
30
|
-
HOST_MODEL_REGISTRY,
|
|
31
|
-
MCPHostType
|
|
32
|
-
)
|
|
33
|
-
from pydantic import ValidationError
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class TestMCPServerConfigBase(unittest.TestCase):
|
|
37
|
-
"""Test suite for MCPServerConfigBase model."""
|
|
38
|
-
|
|
39
|
-
@regression_test
|
|
40
|
-
def test_base_model_local_server_validation_success(self):
|
|
41
|
-
"""Test successful local server configuration with type inference."""
|
|
42
|
-
config = MCPServerConfigBase(
|
|
43
|
-
name="test-server",
|
|
44
|
-
command="python",
|
|
45
|
-
args=["server.py"],
|
|
46
|
-
env={"API_KEY": "test"}
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
self.assertEqual(config.command, "python")
|
|
50
|
-
self.assertEqual(config.type, "stdio") # Inferred from command
|
|
51
|
-
self.assertEqual(len(config.args), 1)
|
|
52
|
-
self.assertEqual(config.env["API_KEY"], "test")
|
|
53
|
-
|
|
54
|
-
@regression_test
|
|
55
|
-
def test_base_model_remote_server_validation_success(self):
|
|
56
|
-
"""Test successful remote server configuration with type inference."""
|
|
57
|
-
config = MCPServerConfigBase(
|
|
58
|
-
name="test-server",
|
|
59
|
-
url="https://api.example.com/mcp",
|
|
60
|
-
headers={"Authorization": "Bearer token"}
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
64
|
-
self.assertEqual(config.type, "sse") # Inferred from url (default to sse)
|
|
65
|
-
self.assertEqual(config.headers["Authorization"], "Bearer token")
|
|
66
|
-
|
|
67
|
-
@regression_test
|
|
68
|
-
def test_base_model_mutual_exclusion_validation_fails(self):
|
|
69
|
-
"""Test validation fails when both command and url provided."""
|
|
70
|
-
with self.assertRaises(ValidationError) as context:
|
|
71
|
-
MCPServerConfigBase(
|
|
72
|
-
name="test-server",
|
|
73
|
-
command="python",
|
|
74
|
-
url="https://api.example.com/mcp"
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
self.assertIn("Cannot specify both 'command' and 'url'", str(context.exception))
|
|
78
|
-
|
|
79
|
-
@regression_test
|
|
80
|
-
def test_base_model_type_field_stdio_validation(self):
|
|
81
|
-
"""Test type=stdio validation."""
|
|
82
|
-
# Valid: type=stdio with command
|
|
83
|
-
config = MCPServerConfigBase(
|
|
84
|
-
name="test-server",
|
|
85
|
-
type="stdio",
|
|
86
|
-
command="python"
|
|
87
|
-
)
|
|
88
|
-
self.assertEqual(config.type, "stdio")
|
|
89
|
-
self.assertEqual(config.command, "python")
|
|
90
|
-
|
|
91
|
-
# Invalid: type=stdio without command
|
|
92
|
-
with self.assertRaises(ValidationError) as context:
|
|
93
|
-
MCPServerConfigBase(
|
|
94
|
-
name="test-server",
|
|
95
|
-
type="stdio",
|
|
96
|
-
url="https://api.example.com/mcp"
|
|
97
|
-
)
|
|
98
|
-
self.assertIn("'command' is required for stdio transport", str(context.exception))
|
|
99
|
-
|
|
100
|
-
@regression_test
|
|
101
|
-
def test_base_model_type_field_sse_validation(self):
|
|
102
|
-
"""Test type=sse validation."""
|
|
103
|
-
# Valid: type=sse with url
|
|
104
|
-
config = MCPServerConfigBase(
|
|
105
|
-
name="test-server",
|
|
106
|
-
type="sse",
|
|
107
|
-
url="https://api.example.com/mcp"
|
|
108
|
-
)
|
|
109
|
-
self.assertEqual(config.type, "sse")
|
|
110
|
-
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
111
|
-
|
|
112
|
-
# Invalid: type=sse without url
|
|
113
|
-
with self.assertRaises(ValidationError) as context:
|
|
114
|
-
MCPServerConfigBase(
|
|
115
|
-
name="test-server",
|
|
116
|
-
type="sse",
|
|
117
|
-
command="python"
|
|
118
|
-
)
|
|
119
|
-
self.assertIn("'url' is required for sse/http transports", str(context.exception))
|
|
120
|
-
|
|
121
|
-
@regression_test
|
|
122
|
-
def test_base_model_type_field_http_validation(self):
|
|
123
|
-
"""Test type=http validation."""
|
|
124
|
-
# Valid: type=http with url
|
|
125
|
-
config = MCPServerConfigBase(
|
|
126
|
-
name="test-server",
|
|
127
|
-
type="http",
|
|
128
|
-
url="https://api.example.com/mcp"
|
|
129
|
-
)
|
|
130
|
-
self.assertEqual(config.type, "http")
|
|
131
|
-
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
132
|
-
|
|
133
|
-
# Invalid: type=http without url
|
|
134
|
-
with self.assertRaises(ValidationError) as context:
|
|
135
|
-
MCPServerConfigBase(
|
|
136
|
-
name="test-server",
|
|
137
|
-
type="http",
|
|
138
|
-
command="python"
|
|
139
|
-
)
|
|
140
|
-
self.assertIn("'url' is required for sse/http transports", str(context.exception))
|
|
141
|
-
|
|
142
|
-
@regression_test
|
|
143
|
-
def test_base_model_type_field_invalid_value(self):
|
|
144
|
-
"""Test validation fails for invalid type value."""
|
|
145
|
-
with self.assertRaises(ValidationError) as context:
|
|
146
|
-
MCPServerConfigBase(
|
|
147
|
-
name="test-server",
|
|
148
|
-
type="invalid",
|
|
149
|
-
command="python"
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
# Pydantic will reject invalid Literal value
|
|
153
|
-
self.assertIn("Input should be 'stdio', 'sse' or 'http'", str(context.exception))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class TestMCPServerConfigGemini(unittest.TestCase):
|
|
157
|
-
"""Test suite for MCPServerConfigGemini model."""
|
|
158
|
-
|
|
159
|
-
@regression_test
|
|
160
|
-
def test_gemini_model_with_all_fields(self):
|
|
161
|
-
"""Test Gemini model with all Gemini-specific fields."""
|
|
162
|
-
config = MCPServerConfigGemini(
|
|
163
|
-
name="gemini-server",
|
|
164
|
-
command="npx",
|
|
165
|
-
args=["-y", "server"],
|
|
166
|
-
env={"API_KEY": "test"},
|
|
167
|
-
cwd="/path/to/dir",
|
|
168
|
-
timeout=30000,
|
|
169
|
-
trust=True,
|
|
170
|
-
includeTools=["tool1", "tool2"],
|
|
171
|
-
excludeTools=["tool3"]
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
# Verify universal fields
|
|
175
|
-
self.assertEqual(config.command, "npx")
|
|
176
|
-
self.assertEqual(config.type, "stdio") # Inferred
|
|
177
|
-
|
|
178
|
-
# Verify Gemini-specific fields
|
|
179
|
-
self.assertEqual(config.cwd, "/path/to/dir")
|
|
180
|
-
self.assertEqual(config.timeout, 30000)
|
|
181
|
-
self.assertTrue(config.trust)
|
|
182
|
-
self.assertEqual(len(config.includeTools), 2)
|
|
183
|
-
self.assertEqual(len(config.excludeTools), 1)
|
|
184
|
-
|
|
185
|
-
@regression_test
|
|
186
|
-
def test_gemini_model_minimal_configuration(self):
|
|
187
|
-
"""Test Gemini model with minimal configuration."""
|
|
188
|
-
config = MCPServerConfigGemini(
|
|
189
|
-
name="gemini-server",
|
|
190
|
-
command="python"
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
self.assertEqual(config.command, "python")
|
|
194
|
-
self.assertEqual(config.type, "stdio") # Inferred
|
|
195
|
-
self.assertIsNone(config.cwd)
|
|
196
|
-
self.assertIsNone(config.timeout)
|
|
197
|
-
self.assertIsNone(config.trust)
|
|
198
|
-
|
|
199
|
-
@regression_test
|
|
200
|
-
def test_gemini_model_field_filtering(self):
|
|
201
|
-
"""Test Gemini model field filtering with model_dump."""
|
|
202
|
-
config = MCPServerConfigGemini(
|
|
203
|
-
name="gemini-server",
|
|
204
|
-
command="python",
|
|
205
|
-
cwd="/path/to/dir"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Use model_dump(exclude_unset=True) to get only set fields
|
|
209
|
-
data = config.model_dump(exclude_unset=True)
|
|
210
|
-
|
|
211
|
-
# Should include name, command, cwd, type (inferred)
|
|
212
|
-
self.assertIn("name", data)
|
|
213
|
-
self.assertIn("command", data)
|
|
214
|
-
self.assertIn("cwd", data)
|
|
215
|
-
self.assertIn("type", data)
|
|
216
|
-
|
|
217
|
-
# Should NOT include unset fields
|
|
218
|
-
self.assertNotIn("timeout", data)
|
|
219
|
-
self.assertNotIn("trust", data)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
class TestMCPServerConfigVSCode(unittest.TestCase):
|
|
223
|
-
"""Test suite for MCPServerConfigVSCode model."""
|
|
224
|
-
|
|
225
|
-
@regression_test
|
|
226
|
-
def test_vscode_model_with_inputs_array(self):
|
|
227
|
-
"""Test VS Code model with inputs array."""
|
|
228
|
-
config = MCPServerConfigVSCode(
|
|
229
|
-
name="vscode-server",
|
|
230
|
-
command="python",
|
|
231
|
-
args=["server.py"],
|
|
232
|
-
inputs=[
|
|
233
|
-
{
|
|
234
|
-
"type": "promptString",
|
|
235
|
-
"id": "api-key",
|
|
236
|
-
"description": "API Key",
|
|
237
|
-
"password": True
|
|
238
|
-
}
|
|
239
|
-
]
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
self.assertEqual(config.command, "python")
|
|
243
|
-
self.assertEqual(len(config.inputs), 1)
|
|
244
|
-
self.assertEqual(config.inputs[0]["id"], "api-key")
|
|
245
|
-
self.assertTrue(config.inputs[0]["password"])
|
|
246
|
-
|
|
247
|
-
@regression_test
|
|
248
|
-
def test_vscode_model_with_envFile(self):
|
|
249
|
-
"""Test VS Code model with envFile field."""
|
|
250
|
-
config = MCPServerConfigVSCode(
|
|
251
|
-
name="vscode-server",
|
|
252
|
-
command="python",
|
|
253
|
-
envFile=".env"
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
self.assertEqual(config.command, "python")
|
|
257
|
-
self.assertEqual(config.envFile, ".env")
|
|
258
|
-
|
|
259
|
-
@regression_test
|
|
260
|
-
def test_vscode_model_minimal_configuration(self):
|
|
261
|
-
"""Test VS Code model with minimal configuration."""
|
|
262
|
-
config = MCPServerConfigVSCode(
|
|
263
|
-
name="vscode-server",
|
|
264
|
-
command="python"
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
self.assertEqual(config.command, "python")
|
|
268
|
-
self.assertEqual(config.type, "stdio") # Inferred
|
|
269
|
-
self.assertIsNone(config.envFile)
|
|
270
|
-
self.assertIsNone(config.inputs)
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
class TestMCPServerConfigCursor(unittest.TestCase):
|
|
274
|
-
"""Test suite for MCPServerConfigCursor model."""
|
|
275
|
-
|
|
276
|
-
@regression_test
|
|
277
|
-
def test_cursor_model_with_envFile(self):
|
|
278
|
-
"""Test Cursor model with envFile field."""
|
|
279
|
-
config = MCPServerConfigCursor(
|
|
280
|
-
name="cursor-server",
|
|
281
|
-
command="python",
|
|
282
|
-
envFile=".env"
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
self.assertEqual(config.command, "python")
|
|
286
|
-
self.assertEqual(config.envFile, ".env")
|
|
287
|
-
|
|
288
|
-
@regression_test
|
|
289
|
-
def test_cursor_model_minimal_configuration(self):
|
|
290
|
-
"""Test Cursor model with minimal configuration."""
|
|
291
|
-
config = MCPServerConfigCursor(
|
|
292
|
-
name="cursor-server",
|
|
293
|
-
command="python"
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
self.assertEqual(config.command, "python")
|
|
297
|
-
self.assertEqual(config.type, "stdio") # Inferred
|
|
298
|
-
self.assertIsNone(config.envFile)
|
|
299
|
-
|
|
300
|
-
@regression_test
|
|
301
|
-
def test_cursor_model_env_with_interpolation_syntax(self):
|
|
302
|
-
"""Test Cursor model with env containing interpolation syntax."""
|
|
303
|
-
# Our code writes the literal string value
|
|
304
|
-
# Cursor handles ${env:NAME}, ${userHome}, etc. expansion at runtime
|
|
305
|
-
config = MCPServerConfigCursor(
|
|
306
|
-
name="cursor-server",
|
|
307
|
-
command="python",
|
|
308
|
-
env={"API_KEY": "${env:API_KEY}", "HOME": "${userHome}"}
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
self.assertEqual(config.env["API_KEY"], "${env:API_KEY}")
|
|
312
|
-
self.assertEqual(config.env["HOME"], "${userHome}")
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
class TestMCPServerConfigClaude(unittest.TestCase):
|
|
316
|
-
"""Test suite for MCPServerConfigClaude model."""
|
|
317
|
-
|
|
318
|
-
@regression_test
|
|
319
|
-
def test_claude_model_universal_fields_only(self):
|
|
320
|
-
"""Test Claude model with universal fields only."""
|
|
321
|
-
config = MCPServerConfigClaude(
|
|
322
|
-
name="claude-server",
|
|
323
|
-
command="python",
|
|
324
|
-
args=["server.py"],
|
|
325
|
-
env={"API_KEY": "test"}
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
# Verify universal fields work
|
|
329
|
-
self.assertEqual(config.command, "python")
|
|
330
|
-
self.assertEqual(config.type, "stdio") # Inferred
|
|
331
|
-
self.assertEqual(len(config.args), 1)
|
|
332
|
-
self.assertEqual(config.env["API_KEY"], "test")
|
|
333
|
-
|
|
334
|
-
@regression_test
|
|
335
|
-
def test_claude_model_all_transport_types(self):
|
|
336
|
-
"""Test Claude model supports all transport types."""
|
|
337
|
-
# stdio transport
|
|
338
|
-
config_stdio = MCPServerConfigClaude(
|
|
339
|
-
name="claude-server",
|
|
340
|
-
type="stdio",
|
|
341
|
-
command="python"
|
|
342
|
-
)
|
|
343
|
-
self.assertEqual(config_stdio.type, "stdio")
|
|
344
|
-
|
|
345
|
-
# sse transport
|
|
346
|
-
config_sse = MCPServerConfigClaude(
|
|
347
|
-
name="claude-server",
|
|
348
|
-
type="sse",
|
|
349
|
-
url="https://api.example.com/mcp"
|
|
350
|
-
)
|
|
351
|
-
self.assertEqual(config_sse.type, "sse")
|
|
352
|
-
|
|
353
|
-
# http transport
|
|
354
|
-
config_http = MCPServerConfigClaude(
|
|
355
|
-
name="claude-server",
|
|
356
|
-
type="http",
|
|
357
|
-
url="https://api.example.com/mcp"
|
|
358
|
-
)
|
|
359
|
-
self.assertEqual(config_http.type, "http")
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
class TestMCPServerConfigOmni(unittest.TestCase):
|
|
363
|
-
"""Test suite for MCPServerConfigOmni model."""
|
|
364
|
-
|
|
365
|
-
@regression_test
|
|
366
|
-
def test_omni_model_all_fields_optional(self):
|
|
367
|
-
"""Test Omni model with no fields (all optional)."""
|
|
368
|
-
# Should not raise ValidationError
|
|
369
|
-
config = MCPServerConfigOmni()
|
|
370
|
-
|
|
371
|
-
self.assertIsNone(config.name)
|
|
372
|
-
self.assertIsNone(config.command)
|
|
373
|
-
self.assertIsNone(config.url)
|
|
374
|
-
|
|
375
|
-
@regression_test
|
|
376
|
-
def test_omni_model_with_mixed_host_fields(self):
|
|
377
|
-
"""Test Omni model with fields from multiple hosts."""
|
|
378
|
-
config = MCPServerConfigOmni(
|
|
379
|
-
name="omni-server",
|
|
380
|
-
command="python",
|
|
381
|
-
cwd="/path/to/dir", # Gemini field
|
|
382
|
-
envFile=".env" # VS Code/Cursor field
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
self.assertEqual(config.command, "python")
|
|
386
|
-
self.assertEqual(config.cwd, "/path/to/dir")
|
|
387
|
-
self.assertEqual(config.envFile, ".env")
|
|
388
|
-
|
|
389
|
-
@regression_test
|
|
390
|
-
def test_omni_model_exclude_unset(self):
|
|
391
|
-
"""Test Omni model with exclude_unset."""
|
|
392
|
-
config = MCPServerConfigOmni(
|
|
393
|
-
name="omni-server",
|
|
394
|
-
command="python",
|
|
395
|
-
args=["server.py"]
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# Use model_dump(exclude_unset=True)
|
|
399
|
-
data = config.model_dump(exclude_unset=True)
|
|
400
|
-
|
|
401
|
-
# Should only include set fields
|
|
402
|
-
self.assertIn("name", data)
|
|
403
|
-
self.assertIn("command", data)
|
|
404
|
-
self.assertIn("args", data)
|
|
405
|
-
|
|
406
|
-
# Should NOT include unset fields
|
|
407
|
-
self.assertNotIn("url", data)
|
|
408
|
-
self.assertNotIn("cwd", data)
|
|
409
|
-
self.assertNotIn("envFile", data)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
class TestHostModelRegistry(unittest.TestCase):
|
|
413
|
-
"""Test suite for HOST_MODEL_REGISTRY dictionary dispatch."""
|
|
414
|
-
|
|
415
|
-
@regression_test
|
|
416
|
-
def test_registry_contains_all_host_types(self):
|
|
417
|
-
"""Test registry contains entries for all MCPHostType values."""
|
|
418
|
-
# Verify registry has entries for all host types
|
|
419
|
-
self.assertIn(MCPHostType.GEMINI, HOST_MODEL_REGISTRY)
|
|
420
|
-
self.assertIn(MCPHostType.CLAUDE_DESKTOP, HOST_MODEL_REGISTRY)
|
|
421
|
-
self.assertIn(MCPHostType.CLAUDE_CODE, HOST_MODEL_REGISTRY)
|
|
422
|
-
self.assertIn(MCPHostType.VSCODE, HOST_MODEL_REGISTRY)
|
|
423
|
-
self.assertIn(MCPHostType.CURSOR, HOST_MODEL_REGISTRY)
|
|
424
|
-
self.assertIn(MCPHostType.LMSTUDIO, HOST_MODEL_REGISTRY)
|
|
425
|
-
|
|
426
|
-
# Verify correct model classes
|
|
427
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.GEMINI], MCPServerConfigGemini)
|
|
428
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.CLAUDE_DESKTOP], MCPServerConfigClaude)
|
|
429
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.CLAUDE_CODE], MCPServerConfigClaude)
|
|
430
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.VSCODE], MCPServerConfigVSCode)
|
|
431
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.CURSOR], MCPServerConfigCursor)
|
|
432
|
-
self.assertEqual(HOST_MODEL_REGISTRY[MCPHostType.LMSTUDIO], MCPServerConfigCursor)
|
|
433
|
-
|
|
434
|
-
@regression_test
|
|
435
|
-
def test_registry_dictionary_dispatch(self):
|
|
436
|
-
"""Test dictionary dispatch retrieves correct model class."""
|
|
437
|
-
# Test Gemini
|
|
438
|
-
gemini_class = HOST_MODEL_REGISTRY[MCPHostType.GEMINI]
|
|
439
|
-
self.assertEqual(gemini_class, MCPServerConfigGemini)
|
|
440
|
-
|
|
441
|
-
# Test VS Code
|
|
442
|
-
vscode_class = HOST_MODEL_REGISTRY[MCPHostType.VSCODE]
|
|
443
|
-
self.assertEqual(vscode_class, MCPServerConfigVSCode)
|
|
444
|
-
|
|
445
|
-
# Test Cursor
|
|
446
|
-
cursor_class = HOST_MODEL_REGISTRY[MCPHostType.CURSOR]
|
|
447
|
-
self.assertEqual(cursor_class, MCPServerConfigCursor)
|
|
448
|
-
|
|
449
|
-
# Test Claude Desktop
|
|
450
|
-
claude_class = HOST_MODEL_REGISTRY[MCPHostType.CLAUDE_DESKTOP]
|
|
451
|
-
self.assertEqual(claude_class, MCPServerConfigClaude)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
class TestFromOmniConversion(unittest.TestCase):
|
|
455
|
-
"""Test suite for from_omni() conversion methods."""
|
|
456
|
-
|
|
457
|
-
@regression_test
|
|
458
|
-
def test_gemini_from_omni_with_supported_fields(self):
|
|
459
|
-
"""Test Gemini from_omni with supported fields."""
|
|
460
|
-
omni = MCPServerConfigOmni(
|
|
461
|
-
name="gemini-server",
|
|
462
|
-
command="npx",
|
|
463
|
-
args=["-y", "server"],
|
|
464
|
-
cwd="/path/to/dir",
|
|
465
|
-
timeout=30000
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
# Convert to Gemini model
|
|
469
|
-
gemini = MCPServerConfigGemini.from_omni(omni)
|
|
470
|
-
|
|
471
|
-
# Verify all supported fields transferred
|
|
472
|
-
self.assertEqual(gemini.name, "gemini-server")
|
|
473
|
-
self.assertEqual(gemini.command, "npx")
|
|
474
|
-
self.assertEqual(len(gemini.args), 2)
|
|
475
|
-
self.assertEqual(gemini.cwd, "/path/to/dir")
|
|
476
|
-
self.assertEqual(gemini.timeout, 30000)
|
|
477
|
-
|
|
478
|
-
@regression_test
|
|
479
|
-
def test_gemini_from_omni_with_unsupported_fields(self):
|
|
480
|
-
"""Test Gemini from_omni excludes unsupported fields."""
|
|
481
|
-
omni = MCPServerConfigOmni(
|
|
482
|
-
name="gemini-server",
|
|
483
|
-
command="python",
|
|
484
|
-
cwd="/path/to/dir", # Gemini field
|
|
485
|
-
envFile=".env" # VS Code field (unsupported by Gemini)
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
# Convert to Gemini model
|
|
489
|
-
gemini = MCPServerConfigGemini.from_omni(omni)
|
|
490
|
-
|
|
491
|
-
# Verify Gemini fields transferred
|
|
492
|
-
self.assertEqual(gemini.command, "python")
|
|
493
|
-
self.assertEqual(gemini.cwd, "/path/to/dir")
|
|
494
|
-
|
|
495
|
-
# Verify unsupported field NOT transferred
|
|
496
|
-
# (Gemini model doesn't have envFile field)
|
|
497
|
-
self.assertFalse(hasattr(gemini, 'envFile') and gemini.envFile is not None)
|
|
498
|
-
|
|
499
|
-
@regression_test
|
|
500
|
-
def test_vscode_from_omni_with_supported_fields(self):
|
|
501
|
-
"""Test VS Code from_omni with supported fields."""
|
|
502
|
-
omni = MCPServerConfigOmni(
|
|
503
|
-
name="vscode-server",
|
|
504
|
-
command="python",
|
|
505
|
-
args=["server.py"],
|
|
506
|
-
envFile=".env",
|
|
507
|
-
inputs=[{"type": "promptString", "id": "api-key"}]
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
# Convert to VS Code model
|
|
511
|
-
vscode = MCPServerConfigVSCode.from_omni(omni)
|
|
512
|
-
|
|
513
|
-
# Verify all supported fields transferred
|
|
514
|
-
self.assertEqual(vscode.name, "vscode-server")
|
|
515
|
-
self.assertEqual(vscode.command, "python")
|
|
516
|
-
self.assertEqual(vscode.envFile, ".env")
|
|
517
|
-
self.assertEqual(len(vscode.inputs), 1)
|
|
518
|
-
|
|
519
|
-
@regression_test
|
|
520
|
-
def test_cursor_from_omni_with_supported_fields(self):
|
|
521
|
-
"""Test Cursor from_omni with supported fields."""
|
|
522
|
-
omni = MCPServerConfigOmni(
|
|
523
|
-
name="cursor-server",
|
|
524
|
-
command="python",
|
|
525
|
-
args=["server.py"],
|
|
526
|
-
envFile=".env"
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
# Convert to Cursor model
|
|
530
|
-
cursor = MCPServerConfigCursor.from_omni(omni)
|
|
531
|
-
|
|
532
|
-
# Verify all supported fields transferred
|
|
533
|
-
self.assertEqual(cursor.name, "cursor-server")
|
|
534
|
-
self.assertEqual(cursor.command, "python")
|
|
535
|
-
self.assertEqual(cursor.envFile, ".env")
|
|
536
|
-
|
|
537
|
-
@regression_test
|
|
538
|
-
def test_claude_from_omni_with_universal_fields(self):
|
|
539
|
-
"""Test Claude from_omni with universal fields only."""
|
|
540
|
-
omni = MCPServerConfigOmni(
|
|
541
|
-
name="claude-server",
|
|
542
|
-
command="python",
|
|
543
|
-
args=["server.py"],
|
|
544
|
-
env={"API_KEY": "test"},
|
|
545
|
-
type="stdio"
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# Convert to Claude model
|
|
549
|
-
claude = MCPServerConfigClaude.from_omni(omni)
|
|
550
|
-
|
|
551
|
-
# Verify universal fields transferred
|
|
552
|
-
self.assertEqual(claude.name, "claude-server")
|
|
553
|
-
self.assertEqual(claude.command, "python")
|
|
554
|
-
self.assertEqual(claude.type, "stdio")
|
|
555
|
-
self.assertEqual(len(claude.args), 1)
|
|
556
|
-
self.assertEqual(claude.env["API_KEY"], "test")
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
class TestGeminiDualTransport(unittest.TestCase):
|
|
560
|
-
"""Test suite for Gemini dual-transport validation (Issue 3)."""
|
|
561
|
-
|
|
562
|
-
@regression_test
|
|
563
|
-
def test_gemini_sse_transport_with_url(self):
|
|
564
|
-
"""Test Gemini SSE transport uses url field."""
|
|
565
|
-
config = MCPServerConfigGemini(
|
|
566
|
-
name="gemini-server",
|
|
567
|
-
type="sse",
|
|
568
|
-
url="https://api.example.com/mcp"
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
self.assertEqual(config.type, "sse")
|
|
572
|
-
self.assertEqual(config.url, "https://api.example.com/mcp")
|
|
573
|
-
self.assertIsNone(config.httpUrl)
|
|
574
|
-
|
|
575
|
-
@regression_test
|
|
576
|
-
def test_gemini_http_transport_with_httpUrl(self):
|
|
577
|
-
"""Test Gemini HTTP transport uses httpUrl field."""
|
|
578
|
-
config = MCPServerConfigGemini(
|
|
579
|
-
name="gemini-server",
|
|
580
|
-
type="http",
|
|
581
|
-
httpUrl="https://api.example.com/mcp"
|
|
582
|
-
)
|
|
583
|
-
|
|
584
|
-
self.assertEqual(config.type, "http")
|
|
585
|
-
self.assertEqual(config.httpUrl, "https://api.example.com/mcp")
|
|
586
|
-
self.assertIsNone(config.url)
|
|
587
|
-
|
|
588
|
-
@regression_test
|
|
589
|
-
def test_gemini_mutual_exclusion_url_and_httpUrl(self):
|
|
590
|
-
"""Test Gemini rejects both url and httpUrl simultaneously."""
|
|
591
|
-
with self.assertRaises(ValidationError) as context:
|
|
592
|
-
MCPServerConfigGemini(
|
|
593
|
-
name="gemini-server",
|
|
594
|
-
url="https://api.example.com/sse",
|
|
595
|
-
httpUrl="https://api.example.com/http"
|
|
596
|
-
)
|
|
597
|
-
|
|
598
|
-
self.assertIn("Cannot specify both 'url' and 'httpUrl'", str(context.exception))
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if __name__ == '__main__':
|
|
602
|
-
unittest.main()
|
|
603
|
-
|