Skip to content

Commit ff80500

Browse files
author
LittleCoinCoin
committed
test: add decorator-based strategy registration validation tests
Implement 10 regression tests validating @register_host_strategy decorator functionality and inheritance patterns following Hatchling patterns. Test coverage: - Decorator registration functionality with automatic strategy discovery - Inheritance pattern validation with Claude and Cursor family bases - Duplicate registration warning logging for strategy overrides - Inheritance validation ensuring MCPHostStrategy subclassing - Unknown host type error handling with available strategy listing - Singleton instance behavior for registered strategies - Host detection functionality with availability checking - Family-based host mappings (claude, cursor families) - Host configuration path resolution through registry - Strategy class validation and error reporting Tests validate decorator-based automatic registration replacing manual patterns, inheritance architecture for code reuse, and proper error handling for unknown or invalid host types. Includes family-based strategy organization tests demonstrating shared functionality between related host types (Claude Desktop/Code, Cursor/LMStudio).
1 parent 391f2b9 commit ff80500

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
"""
2+
Test suite for decorator-based host registry.
3+
4+
This module tests the decorator-based strategy registration system
5+
following Hatchling patterns with inheritance validation.
6+
"""
7+
8+
import unittest
9+
import sys
10+
from pathlib import Path
11+
12+
# Add the parent directory to the path to import wobble
13+
sys.path.insert(0, str(Path(__file__).parent.parent))
14+
15+
try:
16+
from wobble.decorators import regression_test, integration_test
17+
except ImportError:
18+
# Fallback decorators if wobble is not available
19+
def regression_test(func):
20+
return func
21+
22+
def integration_test(scope="component"):
23+
def decorator(func):
24+
return func
25+
return decorator
26+
27+
from hatch.mcp_host_config.host_management import MCPHostRegistry, register_host_strategy, MCPHostStrategy
28+
from hatch.mcp_host_config.models import MCPHostType, MCPServerConfig, HostConfiguration
29+
from pathlib import Path
30+
31+
32+
class TestMCPHostRegistryDecorator(unittest.TestCase):
33+
"""Test suite for decorator-based host registry."""
34+
35+
def setUp(self):
36+
"""Set up test environment."""
37+
# Clear registry before each test
38+
MCPHostRegistry._strategies.clear()
39+
MCPHostRegistry._instances.clear()
40+
41+
def tearDown(self):
42+
"""Clean up test environment."""
43+
# Clear registry after each test
44+
MCPHostRegistry._strategies.clear()
45+
MCPHostRegistry._instances.clear()
46+
47+
@regression_test
48+
def test_decorator_registration_functionality(self):
49+
"""Test that decorator registration works correctly."""
50+
51+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
52+
class TestClaudeStrategy(MCPHostStrategy):
53+
def get_config_path(self):
54+
return Path("/test/path")
55+
def is_host_available(self):
56+
return True
57+
def read_configuration(self):
58+
return HostConfiguration()
59+
def write_configuration(self, config, no_backup=False):
60+
return True
61+
def validate_server_config(self, server_config):
62+
return True
63+
64+
# Verify registration
65+
self.assertIn(MCPHostType.CLAUDE_DESKTOP, MCPHostRegistry._strategies)
66+
self.assertEqual(
67+
MCPHostRegistry._strategies[MCPHostType.CLAUDE_DESKTOP],
68+
TestClaudeStrategy
69+
)
70+
71+
# Verify instance creation
72+
strategy = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
73+
self.assertIsInstance(strategy, TestClaudeStrategy)
74+
75+
@regression_test
76+
def test_decorator_registration_with_inheritance(self):
77+
"""Test decorator registration with inheritance patterns."""
78+
79+
class TestClaudeBase(MCPHostStrategy):
80+
def __init__(self):
81+
self.company_origin = "Anthropic"
82+
self.config_format = "claude_format"
83+
84+
def get_config_key(self):
85+
return "mcpServers"
86+
87+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
88+
class TestClaudeDesktop(TestClaudeBase):
89+
def get_config_path(self):
90+
return Path("/test/claude")
91+
def is_host_available(self):
92+
return True
93+
def read_configuration(self):
94+
return HostConfiguration()
95+
def write_configuration(self, config, no_backup=False):
96+
return True
97+
def validate_server_config(self, server_config):
98+
return True
99+
100+
strategy = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
101+
102+
# Verify inheritance properties
103+
self.assertEqual(strategy.company_origin, "Anthropic")
104+
self.assertEqual(strategy.config_format, "claude_format")
105+
self.assertEqual(strategy.get_config_key(), "mcpServers")
106+
self.assertIsInstance(strategy, TestClaudeBase)
107+
108+
@regression_test
109+
def test_decorator_registration_duplicate_warning(self):
110+
"""Test warning on duplicate strategy registration."""
111+
import logging
112+
113+
class BaseTestStrategy(MCPHostStrategy):
114+
def get_config_path(self):
115+
return Path("/test")
116+
def is_host_available(self):
117+
return True
118+
def read_configuration(self):
119+
return HostConfiguration()
120+
def write_configuration(self, config, no_backup=False):
121+
return True
122+
def validate_server_config(self, server_config):
123+
return True
124+
125+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
126+
class FirstStrategy(BaseTestStrategy):
127+
pass
128+
129+
# Register second strategy for same host type - should log warning
130+
with self.assertLogs('hatch.mcp_host_config.host_management', level='WARNING') as log:
131+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
132+
class SecondStrategy(BaseTestStrategy):
133+
pass
134+
135+
# Verify warning was logged
136+
self.assertTrue(any("Overriding existing strategy" in message for message in log.output))
137+
138+
# Verify second strategy is now registered
139+
strategy = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
140+
self.assertIsInstance(strategy, SecondStrategy)
141+
142+
@regression_test
143+
def test_decorator_registration_inheritance_validation(self):
144+
"""Test that decorator validates inheritance from MCPHostStrategy."""
145+
146+
# Should raise ValueError for non-MCPHostStrategy class
147+
with self.assertRaises(ValueError) as context:
148+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
149+
class InvalidStrategy: # Does not inherit from MCPHostStrategy
150+
pass
151+
152+
self.assertIn("must inherit from MCPHostStrategy", str(context.exception))
153+
154+
@regression_test
155+
def test_registry_get_strategy_unknown_host_type(self):
156+
"""Test error handling for unknown host type."""
157+
# Clear registry to ensure no strategies are registered
158+
MCPHostRegistry._strategies.clear()
159+
160+
with self.assertRaises(ValueError) as context:
161+
MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
162+
163+
self.assertIn("Unknown host type", str(context.exception))
164+
self.assertIn("Available: []", str(context.exception))
165+
166+
@regression_test
167+
def test_registry_singleton_instance_behavior(self):
168+
"""Test that registry returns singleton instances."""
169+
170+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
171+
class TestStrategy(MCPHostStrategy):
172+
def __init__(self):
173+
self.instance_id = id(self)
174+
175+
def get_config_path(self):
176+
return Path("/test")
177+
def is_host_available(self):
178+
return True
179+
def read_configuration(self):
180+
return HostConfiguration()
181+
def write_configuration(self, config, no_backup=False):
182+
return True
183+
def validate_server_config(self, server_config):
184+
return True
185+
186+
# Get strategy multiple times
187+
strategy1 = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
188+
strategy2 = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
189+
190+
# Should be the same instance
191+
self.assertIs(strategy1, strategy2)
192+
self.assertEqual(strategy1.instance_id, strategy2.instance_id)
193+
194+
@regression_test
195+
def test_registry_detect_available_hosts(self):
196+
"""Test host detection functionality."""
197+
198+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
199+
class AvailableStrategy(MCPHostStrategy):
200+
def get_config_path(self):
201+
return Path("/test")
202+
def is_host_available(self):
203+
return True # Available
204+
def read_configuration(self):
205+
return HostConfiguration()
206+
def write_configuration(self, config, no_backup=False):
207+
return True
208+
def validate_server_config(self, server_config):
209+
return True
210+
211+
@register_host_strategy(MCPHostType.CURSOR)
212+
class UnavailableStrategy(MCPHostStrategy):
213+
def get_config_path(self):
214+
return Path("/test")
215+
def is_host_available(self):
216+
return False # Not available
217+
def read_configuration(self):
218+
return HostConfiguration()
219+
def write_configuration(self, config, no_backup=False):
220+
return True
221+
def validate_server_config(self, server_config):
222+
return True
223+
224+
@register_host_strategy(MCPHostType.VSCODE)
225+
class ErrorStrategy(MCPHostStrategy):
226+
def get_config_path(self):
227+
return Path("/test")
228+
def is_host_available(self):
229+
raise Exception("Detection error") # Error during detection
230+
def read_configuration(self):
231+
return HostConfiguration()
232+
def write_configuration(self, config, no_backup=False):
233+
return True
234+
def validate_server_config(self, server_config):
235+
return True
236+
237+
available_hosts = MCPHostRegistry.detect_available_hosts()
238+
239+
# Only the available strategy should be detected
240+
self.assertIn(MCPHostType.CLAUDE_DESKTOP, available_hosts)
241+
self.assertNotIn(MCPHostType.CURSOR, available_hosts)
242+
self.assertNotIn(MCPHostType.VSCODE, available_hosts)
243+
244+
@regression_test
245+
def test_registry_family_mappings(self):
246+
"""Test family host mappings."""
247+
claude_family = MCPHostRegistry.get_family_hosts("claude")
248+
cursor_family = MCPHostRegistry.get_family_hosts("cursor")
249+
unknown_family = MCPHostRegistry.get_family_hosts("unknown")
250+
251+
# Verify family mappings
252+
self.assertIn(MCPHostType.CLAUDE_DESKTOP, claude_family)
253+
self.assertIn(MCPHostType.CLAUDE_CODE, claude_family)
254+
self.assertIn(MCPHostType.CURSOR, cursor_family)
255+
self.assertIn(MCPHostType.LMSTUDIO, cursor_family)
256+
self.assertEqual(unknown_family, [])
257+
258+
@regression_test
259+
def test_registry_get_host_config_path(self):
260+
"""Test getting host configuration path through registry."""
261+
262+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
263+
class TestStrategy(MCPHostStrategy):
264+
def get_config_path(self):
265+
return Path("/test/claude/config.json")
266+
def is_host_available(self):
267+
return True
268+
def read_configuration(self):
269+
return HostConfiguration()
270+
def write_configuration(self, config, no_backup=False):
271+
return True
272+
def validate_server_config(self, server_config):
273+
return True
274+
275+
config_path = MCPHostRegistry.get_host_config_path(MCPHostType.CLAUDE_DESKTOP)
276+
self.assertEqual(config_path, Path("/test/claude/config.json"))
277+
278+
279+
class TestFamilyBasedStrategyRegistration(unittest.TestCase):
280+
"""Test suite for family-based strategy registration with decorators."""
281+
282+
def setUp(self):
283+
"""Set up test environment."""
284+
# Clear registry before each test
285+
MCPHostRegistry._strategies.clear()
286+
MCPHostRegistry._instances.clear()
287+
288+
def tearDown(self):
289+
"""Clean up test environment."""
290+
# Clear registry after each test
291+
MCPHostRegistry._strategies.clear()
292+
MCPHostRegistry._instances.clear()
293+
294+
@regression_test
295+
def test_claude_family_decorator_registration(self):
296+
"""Test Claude family strategies register with decorators."""
297+
298+
class TestClaudeBase(MCPHostStrategy):
299+
def __init__(self):
300+
self.company_origin = "Anthropic"
301+
self.config_format = "claude_format"
302+
303+
def validate_server_config(self, server_config):
304+
# Claude family requires absolute paths
305+
if server_config.command:
306+
return Path(server_config.command).is_absolute()
307+
return True
308+
309+
@register_host_strategy(MCPHostType.CLAUDE_DESKTOP)
310+
class TestClaudeDesktop(TestClaudeBase):
311+
def get_config_path(self):
312+
return Path("/test/claude_desktop")
313+
def is_host_available(self):
314+
return True
315+
def read_configuration(self):
316+
return HostConfiguration()
317+
def write_configuration(self, config, no_backup=False):
318+
return True
319+
320+
@register_host_strategy(MCPHostType.CLAUDE_CODE)
321+
class TestClaudeCode(TestClaudeBase):
322+
def get_config_path(self):
323+
return Path("/test/claude_code")
324+
def is_host_available(self):
325+
return True
326+
def read_configuration(self):
327+
return HostConfiguration()
328+
def write_configuration(self, config, no_backup=False):
329+
return True
330+
331+
# Verify both strategies are registered
332+
claude_desktop = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_DESKTOP)
333+
claude_code = MCPHostRegistry.get_strategy(MCPHostType.CLAUDE_CODE)
334+
335+
# Verify inheritance properties
336+
self.assertEqual(claude_desktop.company_origin, "Anthropic")
337+
self.assertEqual(claude_code.company_origin, "Anthropic")
338+
self.assertIsInstance(claude_desktop, TestClaudeBase)
339+
self.assertIsInstance(claude_code, TestClaudeBase)
340+
341+
# Verify family mappings
342+
claude_family = MCPHostRegistry.get_family_hosts("claude")
343+
self.assertIn(MCPHostType.CLAUDE_DESKTOP, claude_family)
344+
self.assertIn(MCPHostType.CLAUDE_CODE, claude_family)
345+
346+
347+
if __name__ == '__main__':
348+
unittest.main()

0 commit comments

Comments
 (0)