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,138 @@
|
|
|
1
|
+
"""Unit tests for MCP Host Adapter protocol compliance.
|
|
2
|
+
|
|
3
|
+
Test IDs: AP-01 to AP-06 (per 02-test_architecture_rebuild_v0.md)
|
|
4
|
+
Scope: Verify all adapters satisfy BaseAdapter protocol contract.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
|
|
10
|
+
from hatch.mcp_host_config.models import MCPServerConfig, MCPHostType
|
|
11
|
+
from hatch.mcp_host_config.adapters import (
|
|
12
|
+
get_adapter,
|
|
13
|
+
BaseAdapter,
|
|
14
|
+
ClaudeAdapter,
|
|
15
|
+
CodexAdapter,
|
|
16
|
+
CursorAdapter,
|
|
17
|
+
GeminiAdapter,
|
|
18
|
+
KiroAdapter,
|
|
19
|
+
LMStudioAdapter,
|
|
20
|
+
VSCodeAdapter,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# All adapter classes to test
|
|
25
|
+
ALL_ADAPTERS = [
|
|
26
|
+
ClaudeAdapter,
|
|
27
|
+
CodexAdapter,
|
|
28
|
+
CursorAdapter,
|
|
29
|
+
GeminiAdapter,
|
|
30
|
+
KiroAdapter,
|
|
31
|
+
LMStudioAdapter,
|
|
32
|
+
VSCodeAdapter,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Map host types to their expected adapter classes
|
|
36
|
+
HOST_ADAPTER_MAP = {
|
|
37
|
+
MCPHostType.CLAUDE_DESKTOP: ClaudeAdapter,
|
|
38
|
+
MCPHostType.CLAUDE_CODE: ClaudeAdapter,
|
|
39
|
+
MCPHostType.CODEX: CodexAdapter,
|
|
40
|
+
MCPHostType.CURSOR: CursorAdapter,
|
|
41
|
+
MCPHostType.GEMINI: GeminiAdapter,
|
|
42
|
+
MCPHostType.KIRO: KiroAdapter,
|
|
43
|
+
MCPHostType.LMSTUDIO: LMStudioAdapter,
|
|
44
|
+
MCPHostType.VSCODE: VSCodeAdapter,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TestAdapterProtocol(unittest.TestCase):
|
|
49
|
+
"""Tests for adapter protocol compliance (AP-01 to AP-06)."""
|
|
50
|
+
|
|
51
|
+
def test_AP01_all_adapters_have_get_supported_fields(self):
|
|
52
|
+
"""AP-01: All adapters have `get_supported_fields()` returning frozenset."""
|
|
53
|
+
for adapter_cls in ALL_ADAPTERS:
|
|
54
|
+
adapter = adapter_cls()
|
|
55
|
+
with self.subTest(adapter=adapter_cls.__name__):
|
|
56
|
+
self.assertTrue(
|
|
57
|
+
hasattr(adapter, 'get_supported_fields'),
|
|
58
|
+
f"{adapter_cls.__name__} missing 'get_supported_fields'"
|
|
59
|
+
)
|
|
60
|
+
self.assertTrue(
|
|
61
|
+
callable(adapter.get_supported_fields),
|
|
62
|
+
f"{adapter_cls.__name__}.get_supported_fields is not callable"
|
|
63
|
+
)
|
|
64
|
+
supported = adapter.get_supported_fields()
|
|
65
|
+
self.assertIsInstance(
|
|
66
|
+
supported, frozenset,
|
|
67
|
+
f"{adapter_cls.__name__}.get_supported_fields() did not return frozenset"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def test_AP02_all_adapters_have_validate(self):
|
|
71
|
+
"""AP-02: All adapters have callable `validate()` method."""
|
|
72
|
+
for adapter_cls in ALL_ADAPTERS:
|
|
73
|
+
adapter = adapter_cls()
|
|
74
|
+
with self.subTest(adapter=adapter_cls.__name__):
|
|
75
|
+
self.assertTrue(
|
|
76
|
+
hasattr(adapter, 'validate'),
|
|
77
|
+
f"{adapter_cls.__name__} missing 'validate'"
|
|
78
|
+
)
|
|
79
|
+
self.assertTrue(
|
|
80
|
+
callable(adapter.validate),
|
|
81
|
+
f"{adapter_cls.__name__}.validate is not callable"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def test_AP03_all_adapters_have_serialize(self):
|
|
85
|
+
"""AP-03: All adapters have callable `serialize()` method."""
|
|
86
|
+
for adapter_cls in ALL_ADAPTERS:
|
|
87
|
+
adapter = adapter_cls()
|
|
88
|
+
with self.subTest(adapter=adapter_cls.__name__):
|
|
89
|
+
self.assertTrue(
|
|
90
|
+
hasattr(adapter, 'serialize'),
|
|
91
|
+
f"{adapter_cls.__name__} missing 'serialize'"
|
|
92
|
+
)
|
|
93
|
+
self.assertTrue(
|
|
94
|
+
callable(adapter.serialize),
|
|
95
|
+
f"{adapter_cls.__name__}.serialize is not callable"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def test_AP04_serialize_never_returns_name(self):
|
|
99
|
+
"""AP-04: `serialize()` never returns `name` field for any adapter."""
|
|
100
|
+
config = MCPServerConfig(name="test-server", command="python")
|
|
101
|
+
|
|
102
|
+
for adapter_cls in ALL_ADAPTERS:
|
|
103
|
+
adapter = adapter_cls()
|
|
104
|
+
with self.subTest(adapter=adapter_cls.__name__):
|
|
105
|
+
result = adapter.serialize(config)
|
|
106
|
+
self.assertNotIn(
|
|
107
|
+
"name", result,
|
|
108
|
+
f"{adapter_cls.__name__}.serialize() returned 'name' field"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def test_AP05_serialize_never_returns_none_values(self):
|
|
112
|
+
"""AP-05: `serialize()` returns no None values."""
|
|
113
|
+
config = MCPServerConfig(name="test-server", command="python")
|
|
114
|
+
|
|
115
|
+
for adapter_cls in ALL_ADAPTERS:
|
|
116
|
+
adapter = adapter_cls()
|
|
117
|
+
with self.subTest(adapter=adapter_cls.__name__):
|
|
118
|
+
result = adapter.serialize(config)
|
|
119
|
+
for key, value in result.items():
|
|
120
|
+
self.assertIsNotNone(
|
|
121
|
+
value,
|
|
122
|
+
f"{adapter_cls.__name__}.serialize() returned None for '{key}'"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def test_AP06_get_adapter_returns_correct_type(self):
|
|
126
|
+
"""AP-06: get_adapter() returns correct adapter for each host type."""
|
|
127
|
+
for host_type, expected_cls in HOST_ADAPTER_MAP.items():
|
|
128
|
+
with self.subTest(host=host_type.value):
|
|
129
|
+
adapter = get_adapter(host_type)
|
|
130
|
+
self.assertIsInstance(
|
|
131
|
+
adapter, expected_cls,
|
|
132
|
+
f"get_adapter({host_type}) returned {type(adapter)}, expected {expected_cls}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
unittest.main()
|
|
138
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Unit tests for adapter registry.
|
|
2
|
+
|
|
3
|
+
Test IDs: AR-01 to AR-08 (per 02-test_architecture_rebuild_v0.md)
|
|
4
|
+
Scope: Registry initialization, adapter lookup, registration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from hatch.mcp_host_config.adapters import (
|
|
10
|
+
AdapterRegistry,
|
|
11
|
+
get_adapter,
|
|
12
|
+
get_default_registry,
|
|
13
|
+
BaseAdapter,
|
|
14
|
+
ClaudeAdapter,
|
|
15
|
+
CodexAdapter,
|
|
16
|
+
CursorAdapter,
|
|
17
|
+
GeminiAdapter,
|
|
18
|
+
KiroAdapter,
|
|
19
|
+
LMStudioAdapter,
|
|
20
|
+
VSCodeAdapter,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestAdapterRegistry(unittest.TestCase):
|
|
25
|
+
"""Tests for AdapterRegistry class (AR-01 to AR-08)."""
|
|
26
|
+
|
|
27
|
+
def setUp(self):
|
|
28
|
+
"""Create a fresh registry for each test."""
|
|
29
|
+
self.registry = AdapterRegistry()
|
|
30
|
+
|
|
31
|
+
def test_AR01_registry_has_all_default_hosts(self):
|
|
32
|
+
"""AR-01: Registry initializes with all default host adapters."""
|
|
33
|
+
expected_hosts = {
|
|
34
|
+
"claude-desktop",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"codex",
|
|
37
|
+
"cursor",
|
|
38
|
+
"gemini",
|
|
39
|
+
"kiro",
|
|
40
|
+
"lmstudio",
|
|
41
|
+
"vscode",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
actual_hosts = set(self.registry.get_supported_hosts())
|
|
45
|
+
|
|
46
|
+
self.assertEqual(actual_hosts, expected_hosts)
|
|
47
|
+
|
|
48
|
+
def test_AR02_get_adapter_returns_correct_type(self):
|
|
49
|
+
"""AR-02: get_adapter() returns adapter with matching host_name."""
|
|
50
|
+
test_cases = [
|
|
51
|
+
("claude-desktop", ClaudeAdapter),
|
|
52
|
+
("claude-code", ClaudeAdapter),
|
|
53
|
+
("codex", CodexAdapter),
|
|
54
|
+
("cursor", CursorAdapter),
|
|
55
|
+
("gemini", GeminiAdapter),
|
|
56
|
+
("kiro", KiroAdapter),
|
|
57
|
+
("lmstudio", LMStudioAdapter),
|
|
58
|
+
("vscode", VSCodeAdapter),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
for host_name, expected_cls in test_cases:
|
|
62
|
+
with self.subTest(host=host_name):
|
|
63
|
+
adapter = self.registry.get_adapter(host_name)
|
|
64
|
+
self.assertIsInstance(adapter, expected_cls)
|
|
65
|
+
self.assertEqual(adapter.host_name, host_name)
|
|
66
|
+
|
|
67
|
+
def test_AR03_get_adapter_raises_for_unknown_host(self):
|
|
68
|
+
"""AR-03: get_adapter() raises KeyError for unknown host."""
|
|
69
|
+
with self.assertRaises(KeyError) as context:
|
|
70
|
+
self.registry.get_adapter("unknown-host")
|
|
71
|
+
|
|
72
|
+
self.assertIn("unknown-host", str(context.exception))
|
|
73
|
+
self.assertIn("Supported hosts", str(context.exception))
|
|
74
|
+
|
|
75
|
+
def test_AR04_has_adapter_returns_true_for_registered(self):
|
|
76
|
+
"""AR-04: has_adapter() returns True for registered hosts."""
|
|
77
|
+
for host_name in self.registry.get_supported_hosts():
|
|
78
|
+
with self.subTest(host=host_name):
|
|
79
|
+
self.assertTrue(self.registry.has_adapter(host_name))
|
|
80
|
+
|
|
81
|
+
def test_AR05_has_adapter_returns_false_for_unknown(self):
|
|
82
|
+
"""AR-05: has_adapter() returns False for unknown hosts."""
|
|
83
|
+
self.assertFalse(self.registry.has_adapter("unknown-host"))
|
|
84
|
+
|
|
85
|
+
def test_AR06_register_adds_new_adapter(self):
|
|
86
|
+
"""AR-06: register() adds a new adapter to registry."""
|
|
87
|
+
# Create a custom adapter for testing
|
|
88
|
+
class CustomAdapter(BaseAdapter):
|
|
89
|
+
@property
|
|
90
|
+
def host_name(self):
|
|
91
|
+
return "custom-host"
|
|
92
|
+
|
|
93
|
+
def get_supported_fields(self):
|
|
94
|
+
return frozenset({"command", "args"})
|
|
95
|
+
|
|
96
|
+
def validate(self, config):
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def serialize(self, config):
|
|
100
|
+
return {"command": config.command}
|
|
101
|
+
|
|
102
|
+
custom = CustomAdapter()
|
|
103
|
+
self.registry.register(custom)
|
|
104
|
+
|
|
105
|
+
self.assertTrue(self.registry.has_adapter("custom-host"))
|
|
106
|
+
self.assertIs(self.registry.get_adapter("custom-host"), custom)
|
|
107
|
+
|
|
108
|
+
def test_AR07_register_raises_for_duplicate(self):
|
|
109
|
+
"""AR-07: register() raises ValueError for duplicate host name."""
|
|
110
|
+
# Try to register another Claude adapter
|
|
111
|
+
duplicate = ClaudeAdapter(variant="desktop")
|
|
112
|
+
|
|
113
|
+
with self.assertRaises(ValueError) as context:
|
|
114
|
+
self.registry.register(duplicate)
|
|
115
|
+
|
|
116
|
+
self.assertIn("claude-desktop", str(context.exception))
|
|
117
|
+
self.assertIn("already registered", str(context.exception))
|
|
118
|
+
|
|
119
|
+
def test_AR08_unregister_removes_adapter(self):
|
|
120
|
+
"""AR-08: unregister() removes adapter from registry."""
|
|
121
|
+
self.assertTrue(self.registry.has_adapter("claude-desktop"))
|
|
122
|
+
|
|
123
|
+
self.registry.unregister("claude-desktop")
|
|
124
|
+
|
|
125
|
+
self.assertFalse(self.registry.has_adapter("claude-desktop"))
|
|
126
|
+
|
|
127
|
+
def test_unregister_raises_for_unknown(self):
|
|
128
|
+
"""unregister() raises KeyError for unknown host."""
|
|
129
|
+
with self.assertRaises(KeyError):
|
|
130
|
+
self.registry.unregister("unknown-host")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestGlobalRegistryFunctions(unittest.TestCase):
|
|
134
|
+
"""Tests for global registry convenience functions."""
|
|
135
|
+
|
|
136
|
+
def test_get_default_registry_returns_singleton(self):
|
|
137
|
+
"""get_default_registry() returns same instance on multiple calls."""
|
|
138
|
+
registry1 = get_default_registry()
|
|
139
|
+
registry2 = get_default_registry()
|
|
140
|
+
|
|
141
|
+
self.assertIs(registry1, registry2)
|
|
142
|
+
|
|
143
|
+
def test_get_adapter_uses_default_registry(self):
|
|
144
|
+
"""get_adapter() function uses the default registry."""
|
|
145
|
+
adapter = get_adapter("claude-desktop")
|
|
146
|
+
|
|
147
|
+
self.assertIsInstance(adapter, ClaudeAdapter)
|
|
148
|
+
self.assertEqual(adapter.host_name, "claude-desktop")
|
|
149
|
+
|
|
150
|
+
def test_get_adapter_raises_for_unknown(self):
|
|
151
|
+
"""get_adapter() function raises KeyError for unknown host."""
|
|
152
|
+
with self.assertRaises(KeyError):
|
|
153
|
+
get_adapter("unknown-host")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
unittest.main()
|
|
158
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Unit tests for MCPServerConfig unified model.
|
|
2
|
+
|
|
3
|
+
Test IDs: UM-01 to UM-07 (per 02-test_architecture_rebuild_v0.md)
|
|
4
|
+
Scope: Unified model validation, field defaults, transport configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from hatch.mcp_host_config.models import MCPServerConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestMCPServerConfig(unittest.TestCase):
|
|
14
|
+
"""Tests for MCPServerConfig unified model (UM-01 to UM-07)."""
|
|
15
|
+
|
|
16
|
+
def test_UM01_valid_stdio_config(self):
|
|
17
|
+
"""UM-01: Valid stdio config with command field."""
|
|
18
|
+
config = MCPServerConfig(name="test", command="python")
|
|
19
|
+
|
|
20
|
+
self.assertEqual(config.command, "python")
|
|
21
|
+
self.assertTrue(config.is_local_server)
|
|
22
|
+
self.assertFalse(config.is_remote_server)
|
|
23
|
+
|
|
24
|
+
def test_UM02_valid_sse_config(self):
|
|
25
|
+
"""UM-02: Valid SSE config with url field."""
|
|
26
|
+
config = MCPServerConfig(name="test", url="https://example.com/mcp")
|
|
27
|
+
|
|
28
|
+
self.assertEqual(config.url, "https://example.com/mcp")
|
|
29
|
+
self.assertFalse(config.is_local_server)
|
|
30
|
+
self.assertTrue(config.is_remote_server)
|
|
31
|
+
|
|
32
|
+
def test_UM03_valid_http_config_gemini(self):
|
|
33
|
+
"""UM-03: Valid HTTP config with httpUrl field (Gemini-style)."""
|
|
34
|
+
config = MCPServerConfig(name="test", httpUrl="https://example.com/http")
|
|
35
|
+
|
|
36
|
+
self.assertEqual(config.httpUrl, "https://example.com/http")
|
|
37
|
+
# httpUrl is considered remote
|
|
38
|
+
self.assertTrue(config.is_remote_server)
|
|
39
|
+
|
|
40
|
+
def test_UM04_allows_command_and_url(self):
|
|
41
|
+
"""UM-04: Unified model allows both command and url (adapters validate)."""
|
|
42
|
+
# The unified model is permissive - adapters enforce host-specific rules
|
|
43
|
+
config = MCPServerConfig(name="test", command="python", url="https://example.com")
|
|
44
|
+
|
|
45
|
+
self.assertEqual(config.command, "python")
|
|
46
|
+
self.assertEqual(config.url, "https://example.com")
|
|
47
|
+
|
|
48
|
+
def test_UM05_reject_no_transport(self):
|
|
49
|
+
"""UM-05: Reject config with no transport specified."""
|
|
50
|
+
with self.assertRaises(ValidationError) as context:
|
|
51
|
+
MCPServerConfig(name="test")
|
|
52
|
+
|
|
53
|
+
self.assertIn("At least one transport must be specified", str(context.exception))
|
|
54
|
+
|
|
55
|
+
def test_UM06_accept_all_fields(self):
|
|
56
|
+
"""UM-06: Accept config with many fields set."""
|
|
57
|
+
config = MCPServerConfig(
|
|
58
|
+
name="full-server",
|
|
59
|
+
command="python",
|
|
60
|
+
args=["-m", "server"],
|
|
61
|
+
env={"API_KEY": "secret"},
|
|
62
|
+
type="stdio",
|
|
63
|
+
cwd="/workspace",
|
|
64
|
+
timeout=30000,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.assertEqual(config.name, "full-server")
|
|
68
|
+
self.assertEqual(config.args, ["-m", "server"])
|
|
69
|
+
self.assertEqual(config.env, {"API_KEY": "secret"})
|
|
70
|
+
self.assertEqual(config.type, "stdio")
|
|
71
|
+
self.assertEqual(config.cwd, "/workspace")
|
|
72
|
+
self.assertEqual(config.timeout, 30000)
|
|
73
|
+
|
|
74
|
+
def test_UM07_extra_fields_allowed(self):
|
|
75
|
+
"""UM-07: Extra/unknown fields are allowed (extra='allow')."""
|
|
76
|
+
# Create config with extra fields via model_construct to bypass validation
|
|
77
|
+
config = MCPServerConfig.model_construct(
|
|
78
|
+
name="test",
|
|
79
|
+
command="python",
|
|
80
|
+
unknown_field="value"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# The model should allow extra fields
|
|
84
|
+
self.assertEqual(config.command, "python")
|
|
85
|
+
|
|
86
|
+
def test_url_format_validation(self):
|
|
87
|
+
"""Test URL format validation - must start with http:// or https://."""
|
|
88
|
+
with self.assertRaises(ValidationError) as context:
|
|
89
|
+
MCPServerConfig(name="test", url="ftp://example.com")
|
|
90
|
+
|
|
91
|
+
self.assertIn("URL must start with http:// or https://", str(context.exception))
|
|
92
|
+
|
|
93
|
+
def test_command_whitespace_stripped(self):
|
|
94
|
+
"""Test command field strips leading/trailing whitespace."""
|
|
95
|
+
config = MCPServerConfig(name="test", command=" python ")
|
|
96
|
+
|
|
97
|
+
self.assertEqual(config.command, "python")
|
|
98
|
+
|
|
99
|
+
def test_command_empty_rejected(self):
|
|
100
|
+
"""Test empty command (after stripping) is rejected."""
|
|
101
|
+
with self.assertRaises(ValidationError):
|
|
102
|
+
MCPServerConfig(name="test", command=" ")
|
|
103
|
+
|
|
104
|
+
def test_serialization_roundtrip(self):
|
|
105
|
+
"""Test JSON serialization roundtrip."""
|
|
106
|
+
config = MCPServerConfig(
|
|
107
|
+
name="roundtrip-test",
|
|
108
|
+
command="python",
|
|
109
|
+
args=["server.py"],
|
|
110
|
+
env={"KEY": "value"},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Serialize to dict
|
|
114
|
+
data = config.model_dump(exclude_none=True)
|
|
115
|
+
|
|
116
|
+
# Reconstruct from dict
|
|
117
|
+
reconstructed = MCPServerConfig.model_validate(data)
|
|
118
|
+
|
|
119
|
+
self.assertEqual(reconstructed.name, config.name)
|
|
120
|
+
self.assertEqual(reconstructed.command, config.command)
|
|
121
|
+
self.assertEqual(reconstructed.args, config.args)
|
|
122
|
+
self.assertEqual(reconstructed.env, config.env)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestMCPServerConfigProperties(unittest.TestCase):
|
|
126
|
+
"""Tests for MCPServerConfig computed properties."""
|
|
127
|
+
|
|
128
|
+
def test_is_local_server_with_command(self):
|
|
129
|
+
"""Local server detection with command."""
|
|
130
|
+
config = MCPServerConfig(name="test", command="python")
|
|
131
|
+
self.assertTrue(config.is_local_server)
|
|
132
|
+
|
|
133
|
+
def test_is_remote_server_with_url(self):
|
|
134
|
+
"""Remote server detection with url."""
|
|
135
|
+
config = MCPServerConfig(name="test", url="https://example.com")
|
|
136
|
+
self.assertTrue(config.is_remote_server)
|
|
137
|
+
|
|
138
|
+
def test_is_remote_server_with_httpUrl(self):
|
|
139
|
+
"""Remote server detection with httpUrl."""
|
|
140
|
+
config = MCPServerConfig(name="test", httpUrl="https://example.com/http")
|
|
141
|
+
self.assertTrue(config.is_remote_server)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
unittest.main()
|
|
146
|
+
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Kiro MCP Integration Tests
|
|
3
|
-
|
|
4
|
-
End-to-end integration tests combining CLI, model conversion, and strategy operations.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import unittest
|
|
8
|
-
from unittest.mock import patch, MagicMock
|
|
9
|
-
|
|
10
|
-
from wobble.decorators import integration_test
|
|
11
|
-
|
|
12
|
-
from hatch.cli_hatch import handle_mcp_configure
|
|
13
|
-
from hatch.mcp_host_config.models import (
|
|
14
|
-
HOST_MODEL_REGISTRY,
|
|
15
|
-
MCPHostType,
|
|
16
|
-
MCPServerConfigKiro
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TestKiroIntegration(unittest.TestCase):
|
|
21
|
-
"""Test suite for end-to-end Kiro integration."""
|
|
22
|
-
|
|
23
|
-
@integration_test(scope="component")
|
|
24
|
-
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
25
|
-
def test_kiro_end_to_end_configuration(self, mock_manager_class):
|
|
26
|
-
"""Test complete Kiro configuration workflow."""
|
|
27
|
-
# Setup mocks
|
|
28
|
-
mock_manager = MagicMock()
|
|
29
|
-
mock_manager_class.return_value = mock_manager
|
|
30
|
-
|
|
31
|
-
mock_result = MagicMock()
|
|
32
|
-
mock_result.success = True
|
|
33
|
-
mock_manager.configure_server.return_value = mock_result
|
|
34
|
-
|
|
35
|
-
# Execute CLI command with Kiro-specific arguments
|
|
36
|
-
result = handle_mcp_configure(
|
|
37
|
-
host='kiro',
|
|
38
|
-
server_name='augment-server',
|
|
39
|
-
command='auggie',
|
|
40
|
-
args=['--mcp', '-m', 'default'],
|
|
41
|
-
disabled=False,
|
|
42
|
-
auto_approve_tools=['codebase-retrieval', 'fetch'],
|
|
43
|
-
disable_tools=['dangerous-tool'],
|
|
44
|
-
auto_approve=True
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# Verify success
|
|
48
|
-
self.assertEqual(result, 0)
|
|
49
|
-
|
|
50
|
-
# Verify configuration manager was called
|
|
51
|
-
mock_manager.configure_server.assert_called_once()
|
|
52
|
-
|
|
53
|
-
# Verify server configuration
|
|
54
|
-
call_args = mock_manager.configure_server.call_args
|
|
55
|
-
server_config = call_args.kwargs['server_config']
|
|
56
|
-
|
|
57
|
-
# Verify all Kiro-specific fields
|
|
58
|
-
self.assertFalse(server_config.disabled)
|
|
59
|
-
self.assertEqual(len(server_config.autoApprove), 2)
|
|
60
|
-
self.assertEqual(len(server_config.disabledTools), 1)
|
|
61
|
-
self.assertIn('codebase-retrieval', server_config.autoApprove)
|
|
62
|
-
self.assertIn('dangerous-tool', server_config.disabledTools)
|
|
63
|
-
|
|
64
|
-
@integration_test(scope="system")
|
|
65
|
-
def test_kiro_host_model_registry_integration(self):
|
|
66
|
-
"""Test Kiro integration with HOST_MODEL_REGISTRY."""
|
|
67
|
-
# Verify Kiro is in registry
|
|
68
|
-
self.assertIn(MCPHostType.KIRO, HOST_MODEL_REGISTRY)
|
|
69
|
-
|
|
70
|
-
# Verify correct model class
|
|
71
|
-
model_class = HOST_MODEL_REGISTRY[MCPHostType.KIRO]
|
|
72
|
-
self.assertEqual(model_class.__name__, "MCPServerConfigKiro")
|
|
73
|
-
|
|
74
|
-
# Test model instantiation
|
|
75
|
-
model_instance = model_class(
|
|
76
|
-
name="test-server",
|
|
77
|
-
command="auggie",
|
|
78
|
-
disabled=True
|
|
79
|
-
)
|
|
80
|
-
self.assertTrue(model_instance.disabled)
|
|
81
|
-
|
|
82
|
-
@integration_test(scope="component")
|
|
83
|
-
def test_kiro_model_to_strategy_workflow(self):
|
|
84
|
-
"""Test workflow from model creation to strategy operations."""
|
|
85
|
-
# Import to trigger registration
|
|
86
|
-
import hatch.mcp_host_config.strategies
|
|
87
|
-
from hatch.mcp_host_config.host_management import MCPHostRegistry
|
|
88
|
-
|
|
89
|
-
# Create Kiro model
|
|
90
|
-
kiro_model = MCPServerConfigKiro(
|
|
91
|
-
name="workflow-test",
|
|
92
|
-
command="auggie",
|
|
93
|
-
args=["--mcp"],
|
|
94
|
-
disabled=False,
|
|
95
|
-
autoApprove=["codebase-retrieval"]
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
# Get Kiro strategy
|
|
99
|
-
strategy = MCPHostRegistry.get_strategy(MCPHostType.KIRO)
|
|
100
|
-
|
|
101
|
-
# Verify strategy can validate the model
|
|
102
|
-
self.assertTrue(strategy.validate_server_config(kiro_model))
|
|
103
|
-
|
|
104
|
-
# Verify model fields are accessible
|
|
105
|
-
self.assertEqual(kiro_model.command, "auggie")
|
|
106
|
-
self.assertFalse(kiro_model.disabled)
|
|
107
|
-
self.assertIn("codebase-retrieval", kiro_model.autoApprove)
|
|
108
|
-
|
|
109
|
-
@integration_test(scope="end_to_end")
|
|
110
|
-
@patch('hatch.cli_hatch.MCPHostConfigurationManager')
|
|
111
|
-
def test_kiro_complete_lifecycle(self, mock_manager_class):
|
|
112
|
-
"""Test complete Kiro server lifecycle: create, configure, validate."""
|
|
113
|
-
# Setup mocks
|
|
114
|
-
mock_manager = MagicMock()
|
|
115
|
-
mock_manager_class.return_value = mock_manager
|
|
116
|
-
|
|
117
|
-
mock_result = MagicMock()
|
|
118
|
-
mock_result.success = True
|
|
119
|
-
mock_manager.configure_server.return_value = mock_result
|
|
120
|
-
|
|
121
|
-
# Step 1: Configure server via CLI
|
|
122
|
-
result = handle_mcp_configure(
|
|
123
|
-
host='kiro',
|
|
124
|
-
server_name='lifecycle-test',
|
|
125
|
-
command='auggie',
|
|
126
|
-
args=['--mcp', '-w', '.'],
|
|
127
|
-
disabled=False,
|
|
128
|
-
auto_approve_tools=['codebase-retrieval'],
|
|
129
|
-
auto_approve=True
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
# Verify CLI success
|
|
133
|
-
self.assertEqual(result, 0)
|
|
134
|
-
|
|
135
|
-
# Step 2: Verify configuration manager interaction
|
|
136
|
-
mock_manager.configure_server.assert_called_once()
|
|
137
|
-
call_args = mock_manager.configure_server.call_args
|
|
138
|
-
|
|
139
|
-
# Step 3: Verify server configuration structure
|
|
140
|
-
server_config = call_args.kwargs['server_config']
|
|
141
|
-
self.assertEqual(server_config.name, 'lifecycle-test')
|
|
142
|
-
self.assertEqual(server_config.command, 'auggie')
|
|
143
|
-
self.assertIn('--mcp', server_config.args)
|
|
144
|
-
self.assertIn('-w', server_config.args)
|
|
145
|
-
self.assertFalse(server_config.disabled)
|
|
146
|
-
self.assertIn('codebase-retrieval', server_config.autoApprove)
|
|
147
|
-
|
|
148
|
-
# Step 4: Verify model type
|
|
149
|
-
self.assertIsInstance(server_config, MCPServerConfigKiro)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if __name__ == '__main__':
|
|
153
|
-
unittest.main()
|