""" Comprehensive Test Suite for DSS MCP Commands Tests all 35 DSS MCP tools across 4 categories: - DSS Core (10 tools) - DevTools (12 tools) - Browser Automation (8 tools) - Context Compiler (5 tools) Tests validate: - Tool definitions and schemas - Required parameters - Implementation presence - Security measures - Error handling patterns """ import pytest import re from pathlib import Path # ============================================================================= # TEST CONFIGURATION # ============================================================================= # Use relative path from project root MCP_SERVER_PATH = Path(__file__).parent.parent.parent.parent / "dss-claude-plugin/servers/dss-mcp-server.py" # Complete tool registry - all 35 MCP tools DSS_CORE_TOOLS = { "dss_analyze_project": { "required": ["path"], "optional": [], "impl_func": "analyze_project" }, "dss_extract_tokens": { "required": ["path"], "optional": ["sources"], "impl_func": "extract_tokens" }, "dss_generate_theme": { "required": ["format"], "optional": ["tokens", "theme_name"], "impl_func": "generate_theme" }, "dss_list_themes": { "required": [], "optional": [], "impl_func": "list_themes" }, "dss_get_status": { "required": [], "optional": ["format"], "impl_func": "get_status" }, "dss_audit_components": { "required": ["path"], "optional": [], "impl_func": "audit_components" }, "dss_setup_storybook": { "required": ["path"], "optional": ["action"], "impl_func": "setup_storybook" }, "dss_sync_figma": { "required": ["file_key"], "optional": [], "impl_func": "sync_figma" }, "dss_find_quick_wins": { "required": ["path"], "optional": [], "impl_func": "find_quick_wins" }, "dss_transform_tokens": { "required": ["tokens", "output_format"], "optional": ["input_format"], "impl_func": "transform_tokens" }, } DEVTOOLS_TOOLS = { "devtools_launch": { "required": [], "optional": ["url", "headless"], "impl_func": "devtools_launch_impl" }, "devtools_connect": { "required": [], "optional": ["port", "host"], "impl_func": "devtools_connect_impl" }, "devtools_disconnect": { "required": [], "optional": [], "impl_func": "devtools_disconnect_impl" }, "devtools_list_pages": { "required": [], "optional": [], "impl_func": "devtools_list_pages_impl" }, "devtools_select_page": { "required": ["page_id"], "optional": [], "impl_func": "devtools_select_page_impl" }, "devtools_console_logs": { "required": [], "optional": ["level", "limit", "clear"], "impl_func": "devtools_console_logs_impl" }, "devtools_network_requests": { "required": [], "optional": ["filter_url", "limit"], "impl_func": "devtools_network_requests_impl" }, "devtools_evaluate": { "required": ["expression"], "optional": [], "impl_func": "devtools_evaluate_impl" }, "devtools_query_dom": { "required": ["selector"], "optional": [], "impl_func": "devtools_query_dom_impl" }, "devtools_goto": { "required": ["url"], "optional": ["wait_until"], "impl_func": "devtools_goto_impl" }, "devtools_screenshot": { "required": [], "optional": ["selector", "full_page"], "impl_func": "devtools_screenshot_impl" }, "devtools_performance": { "required": [], "optional": [], "impl_func": "devtools_performance_impl" }, } BROWSER_TOOLS = { "browser_init": { "required": [], "optional": ["mode", "url", "session_id", "headless"], "impl_func": "browser_init_impl" }, "browser_get_logs": { "required": [], "optional": ["level", "limit"], "impl_func": "browser_get_logs_impl" }, "browser_screenshot": { "required": [], "optional": ["selector", "full_page"], "impl_func": "browser_screenshot_impl" }, "browser_dom_snapshot": { "required": [], "optional": [], "impl_func": "browser_dom_snapshot_impl" }, "browser_get_errors": { "required": [], "optional": ["limit"], "impl_func": "browser_get_errors_impl" }, "browser_accessibility_audit": { "required": [], "optional": ["selector"], "impl_func": "browser_accessibility_audit_impl" }, "browser_performance": { "required": [], "optional": [], "impl_func": "browser_performance_impl" }, "browser_close": { "required": [], "optional": [], "impl_func": "browser_close_impl" }, } CONTEXT_COMPILER_TOOLS = { "dss_get_resolved_context": { "required": ["manifest_path"], "optional": ["debug", "force_refresh"], "impl_func": None # Handled inline in dispatcher }, "dss_resolve_token": { "required": ["manifest_path", "token_path"], "optional": ["force_refresh"], "impl_func": None }, "dss_validate_manifest": { "required": ["manifest_path"], "optional": [], "impl_func": None }, "dss_list_skins": { "required": [], "optional": [], "impl_func": None }, "dss_get_compiler_status": { "required": [], "optional": [], "impl_func": None }, } ALL_TOOLS = { **DSS_CORE_TOOLS, **DEVTOOLS_TOOLS, **BROWSER_TOOLS, **CONTEXT_COMPILER_TOOLS, } # ============================================================================= # FIXTURES # ============================================================================= @pytest.fixture def mcp_server_content(): """Load MCP server source code.""" return MCP_SERVER_PATH.read_text() # ============================================================================= # TEST CLASS: Tool Definitions # ============================================================================= class TestToolDefinitions: """Verify all 48 tools are properly defined in the MCP server.""" def test_total_tool_count(self, mcp_server_content): """Verify we have exactly 48 tools defined.""" # Count Tool( occurrences tool_definitions = re.findall(r'Tool\(\s*name="([^"]+)"', mcp_server_content) assert len(tool_definitions) == 48, f"Expected 48 tools, found {len(tool_definitions)}" @pytest.mark.parametrize("tool_name", DSS_CORE_TOOLS.keys()) def test_dss_core_tool_defined(self, mcp_server_content, tool_name): """Verify each DSS core tool is defined.""" assert f'name="{tool_name}"' in mcp_server_content, f"Tool {tool_name} not found" @pytest.mark.parametrize("tool_name", DEVTOOLS_TOOLS.keys()) def test_devtools_tool_defined(self, mcp_server_content, tool_name): """Verify each DevTools tool is defined.""" assert f'name="{tool_name}"' in mcp_server_content, f"Tool {tool_name} not found" @pytest.mark.parametrize("tool_name", BROWSER_TOOLS.keys()) def test_browser_tool_defined(self, mcp_server_content, tool_name): """Verify each Browser automation tool is defined.""" assert f'name="{tool_name}"' in mcp_server_content, f"Tool {tool_name} not found" @pytest.mark.parametrize("tool_name", CONTEXT_COMPILER_TOOLS.keys()) def test_context_compiler_tool_defined(self, mcp_server_content, tool_name): """Verify each Context Compiler tool is defined.""" assert f'name="{tool_name}"' in mcp_server_content, f"Tool {tool_name} not found" # ============================================================================= # TEST CLASS: Tool Dispatcher # ============================================================================= class TestToolDispatcher: """Verify tool dispatcher handles all tools.""" @pytest.mark.parametrize("tool_name", ALL_TOOLS.keys()) def test_tool_in_dispatcher(self, mcp_server_content, tool_name): """Verify each tool has a dispatcher case.""" # Check for: elif name == "tool_name" or if name == "tool_name" pattern = rf'(if|elif)\s+name\s*==\s*"{tool_name}"' assert re.search(pattern, mcp_server_content), f"Tool {tool_name} not in dispatcher" # ============================================================================= # TEST CLASS: Implementation Functions # ============================================================================= class TestImplementationFunctions: """Verify implementation functions exist.""" @pytest.mark.parametrize("tool_name,config", [ (k, v) for k, v in DSS_CORE_TOOLS.items() if v["impl_func"] ]) def test_dss_core_impl_exists(self, mcp_server_content, tool_name, config): """Verify DSS core tool implementations exist.""" impl_func = config["impl_func"] pattern = rf'async def {impl_func}\(' assert re.search(pattern, mcp_server_content), f"Implementation {impl_func} not found for {tool_name}" @pytest.mark.parametrize("tool_name,config", [ (k, v) for k, v in DEVTOOLS_TOOLS.items() if v["impl_func"] ]) def test_devtools_impl_exists(self, mcp_server_content, tool_name, config): """Verify DevTools implementations exist.""" impl_func = config["impl_func"] pattern = rf'async def {impl_func}\(' assert re.search(pattern, mcp_server_content), f"Implementation {impl_func} not found for {tool_name}" @pytest.mark.parametrize("tool_name,config", [ (k, v) for k, v in BROWSER_TOOLS.items() if v["impl_func"] ]) def test_browser_impl_exists(self, mcp_server_content, tool_name, config): """Verify Browser tool implementations exist.""" impl_func = config["impl_func"] pattern = rf'async def {impl_func}\(' assert re.search(pattern, mcp_server_content), f"Implementation {impl_func} not found for {tool_name}" # ============================================================================= # TEST CLASS: Input Schemas # ============================================================================= class TestInputSchemas: """Verify input schemas are properly defined.""" def test_all_tools_have_input_schema(self, mcp_server_content): """Verify all tools have inputSchema defined.""" tool_definitions = re.findall(r'Tool\(\s*name="([^"]+)"', mcp_server_content) for tool in tool_definitions: # Find Tool definition and check for inputSchema pattern = rf'name="{tool}".*?inputSchema' assert re.search(pattern, mcp_server_content, re.DOTALL), f"Tool {tool} missing inputSchema" @pytest.mark.parametrize("tool_name,config", list(ALL_TOOLS.items())) def test_required_params_in_schema(self, mcp_server_content, tool_name, config): """Verify required parameters are marked in schema.""" if not config["required"]: return # Skip tools with no required params # Find the tool's schema section tool_pattern = rf'name="{tool_name}".*?inputSchema=\{{(.*?)\}}\s*\)' match = re.search(tool_pattern, mcp_server_content, re.DOTALL) if match: schema_content = match.group(1) # Check for "required": [...] with our params for param in config["required"]: # The param should appear in the required array or properties assert param in schema_content, f"Required param '{param}' not in schema for {tool_name}" # ============================================================================= # TEST CLASS: Security Measures # ============================================================================= class TestSecurityMeasures: """Verify security measures are in place.""" def test_audit_logging_for_evaluate(self, mcp_server_content): """Verify devtools_evaluate has audit logging.""" # Check for AUDIT log in devtools_evaluate_impl pattern = r'def devtools_evaluate_impl.*?\[AUDIT\]' assert re.search(pattern, mcp_server_content, re.DOTALL), "devtools_evaluate missing audit logging" def test_playwright_availability_check(self, mcp_server_content): """Verify Playwright availability is checked before DevTools operations.""" assert "PLAYWRIGHT_AVAILABLE" in mcp_server_content, "Missing Playwright availability check" assert 'not PLAYWRIGHT_AVAILABLE and name.startswith("devtools_")' in mcp_server_content def test_dss_availability_check(self, mcp_server_content): """Verify DSS availability is checked before DSS operations.""" assert "DSS_AVAILABLE" in mcp_server_content, "Missing DSS availability check" assert 'not DSS_AVAILABLE and name.startswith("dss_")' in mcp_server_content def test_context_compiler_availability_check(self, mcp_server_content): """Verify Context Compiler availability is checked.""" assert "CONTEXT_COMPILER_AVAILABLE" in mcp_server_content, "Missing Context Compiler availability check" def test_figma_token_validation(self, mcp_server_content): """Verify Figma sync checks for API token.""" assert 'FIGMA_TOKEN' in mcp_server_content, "Missing Figma token check" # Should return error if token not configured assert 'FIGMA_TOKEN not configured' in mcp_server_content def test_path_validation(self, mcp_server_content): """Verify path validation is performed.""" # Check that Path.resolve() is used for path inputs assert "Path(path).resolve()" in mcp_server_content, "Missing path resolution" # Check for existence validation assert "not project_path.exists()" in mcp_server_content or "not target_path.exists()" in mcp_server_content # ============================================================================= # TEST CLASS: Async/Timeout Handling # ============================================================================= class TestAsyncHandling: """Verify async operations are properly handled.""" def test_timeout_decorator_exists(self, mcp_server_content): """Verify timeout decorator is defined.""" assert "def with_timeout" in mcp_server_content, "Missing timeout decorator" def test_timeout_config_exists(self, mcp_server_content): """Verify timeout configuration is defined.""" assert "TIMEOUT_CONFIG" in mcp_server_content, "Missing timeout configuration" # Check for expected timeout keys expected_keys = ["analyze", "extract", "generate", "figma_api", "storybook", "devtools_connect"] for key in expected_keys: assert f'"{key}"' in mcp_server_content, f"Missing timeout key: {key}" def test_devtools_timeout_applied(self, mcp_server_content): """Verify DevTools operations have timeouts.""" # Check for @with_timeout decorator on critical functions assert '@with_timeout("devtools_connect")' in mcp_server_content def test_run_in_executor_usage(self, mcp_server_content): """Verify blocking operations use run_in_executor.""" assert "loop.run_in_executor" in mcp_server_content, "Missing run_in_executor for blocking operations" # ============================================================================= # TEST CLASS: State Management # ============================================================================= class TestStateManagement: """Verify state management classes are properly defined.""" def test_devtools_state_class(self, mcp_server_content): """Verify DevToolsState dataclass is defined.""" assert "class DevToolsState:" in mcp_server_content assert "@dataclass" in mcp_server_content def test_browser_automation_state_class(self, mcp_server_content): """Verify BrowserAutomationState dataclass is defined.""" assert "class BrowserAutomationState:" in mcp_server_content def test_devtools_state_instance(self, mcp_server_content): """Verify DevTools state instance is created.""" assert "devtools = DevToolsState()" in mcp_server_content def test_browser_state_instance(self, mcp_server_content): """Verify Browser state instance is created.""" assert "browser_state = BrowserAutomationState()" in mcp_server_content def test_bounded_buffers(self, mcp_server_content): """Verify bounded deques are used for log capture.""" assert "deque(maxlen=" in mcp_server_content, "Missing bounded deque for log capture" assert "DEVTOOLS_CONSOLE_MAX_ENTRIES" in mcp_server_content assert "DEVTOOLS_NETWORK_MAX_ENTRIES" in mcp_server_content # ============================================================================= # TEST CLASS: Error Handling # ============================================================================= class TestErrorHandling: """Verify error handling patterns.""" def test_try_except_in_dispatcher(self, mcp_server_content): """Verify dispatcher has error handling.""" assert "except Exception as e:" in mcp_server_content assert '"error":' in mcp_server_content or "'error':" in mcp_server_content def test_safe_serialize_function(self, mcp_server_content): """Verify safe_serialize function exists for JSON serialization.""" assert "def safe_serialize" in mcp_server_content def test_import_error_handling(self, mcp_server_content): """Verify import errors are captured.""" assert "except ImportError" in mcp_server_content assert "DSS_IMPORT_ERROR" in mcp_server_content assert "CONTEXT_COMPILER_IMPORT_ERROR" in mcp_server_content # ============================================================================= # TEST CLASS: Browser Automation Modes # ============================================================================= class TestBrowserAutomationModes: """Verify Browser automation supports LOCAL and REMOTE modes.""" def test_local_mode_support(self, mcp_server_content): """Verify LOCAL mode is supported.""" assert 'mode == "local"' in mcp_server_content assert "LocalBrowserStrategy" in mcp_server_content def test_remote_mode_support(self, mcp_server_content): """Verify REMOTE mode is supported.""" assert 'mode == "remote"' in mcp_server_content assert "remote_api_url" in mcp_server_content assert "session_id" in mcp_server_content def test_aiohttp_for_remote(self, mcp_server_content): """Verify aiohttp is used for remote API calls.""" assert "import aiohttp" in mcp_server_content assert "aiohttp.ClientSession()" in mcp_server_content # ============================================================================= # TEST CLASS: Server Configuration # ============================================================================= class TestServerConfiguration: """Verify server is properly configured.""" def test_mcp_server_created(self, mcp_server_content): """Verify MCP server instance is created.""" assert 'server = Server("dss-server")' in mcp_server_content def test_list_tools_decorator(self, mcp_server_content): """Verify list_tools is registered.""" assert "@server.list_tools()" in mcp_server_content def test_call_tool_decorator(self, mcp_server_content): """Verify call_tool is registered.""" assert "@server.call_tool()" in mcp_server_content def test_main_function(self, mcp_server_content): """Verify main function exists.""" assert "async def main():" in mcp_server_content assert 'if __name__ == "__main__":' in mcp_server_content def test_stdio_server_usage(self, mcp_server_content): """Verify stdio_server is used for transport.""" assert "stdio_server" in mcp_server_content assert "async with stdio_server()" in mcp_server_content # ============================================================================= # TEST CLASS: Cleanup Handling # ============================================================================= class TestCleanupHandling: """Verify cleanup is properly handled.""" def test_disconnect_cleanup(self, mcp_server_content): """Verify DevTools disconnect cleans up properly.""" # Should reset state assert "devtools = DevToolsState()" in mcp_server_content # Should remove event listeners assert "remove_listener" in mcp_server_content def test_browser_close_cleanup(self, mcp_server_content): """Verify browser close cleans up properly.""" assert "browser_state = BrowserAutomationState()" in mcp_server_content def test_main_finally_cleanup(self, mcp_server_content): """Verify main function has cleanup in finally block.""" # Check for cleanup on server shutdown assert "finally:" in mcp_server_content assert "devtools_disconnect_impl()" in mcp_server_content assert "browser_close_impl()" in mcp_server_content # ============================================================================= # TEST CLASS: Category Counts # ============================================================================= class TestCategoryCounts: """Verify tool counts per category.""" def test_dss_core_count(self): """Verify DSS core has 10 tools.""" assert len(DSS_CORE_TOOLS) == 10, f"Expected 10 DSS core tools, got {len(DSS_CORE_TOOLS)}" def test_devtools_count(self): """Verify DevTools has 12 tools.""" assert len(DEVTOOLS_TOOLS) == 12, f"Expected 12 DevTools tools, got {len(DEVTOOLS_TOOLS)}" def test_browser_count(self): """Verify Browser automation has 8 tools.""" assert len(BROWSER_TOOLS) == 8, f"Expected 8 Browser tools, got {len(BROWSER_TOOLS)}" def test_context_compiler_count(self): """Verify Context Compiler has 5 tools.""" assert len(CONTEXT_COMPILER_TOOLS) == 5, f"Expected 5 Context Compiler tools, got {len(CONTEXT_COMPILER_TOOLS)}" def test_total_count(self): """Verify total registered tools in test suite (subset of all 48 server tools).""" total = len(DSS_CORE_TOOLS) + len(DEVTOOLS_TOOLS) + len(BROWSER_TOOLS) + len(CONTEXT_COMPILER_TOOLS) assert total == 35, f"Expected 35 registered test tools, got {total}" # ============================================================================= # TEST CLASS: DSS Core Functionality # ============================================================================= class TestDSSCoreFunctionality: """Test DSS core tool specific requirements.""" def test_project_scanner_usage(self, mcp_server_content): """Verify ProjectScanner is used for analysis.""" assert "ProjectScanner" in mcp_server_content def test_react_analyzer_usage(self, mcp_server_content): """Verify ReactAnalyzer is used for component analysis.""" assert "ReactAnalyzer" in mcp_server_content def test_style_analyzer_usage(self, mcp_server_content): """Verify StyleAnalyzer is used for style analysis.""" assert "StyleAnalyzer" in mcp_server_content def test_token_sources(self, mcp_server_content): """Verify all token sources are available.""" sources = ["CSSTokenSource", "SCSSTokenSource", "TailwindTokenSource", "JSONTokenSource"] for source in sources: assert source in mcp_server_content, f"Missing token source: {source}" def test_token_merger_usage(self, mcp_server_content): """Verify TokenMerger is used for combining tokens.""" assert "TokenMerger" in mcp_server_content assert "MergeStrategy" in mcp_server_content def test_storybook_support(self, mcp_server_content): """Verify Storybook classes are used.""" classes = ["StorybookScanner", "StoryGenerator", "ThemeGenerator"] for cls in classes: assert cls in mcp_server_content, f"Missing Storybook class: {cls}" # ============================================================================= # TEST CLASS: DevTools Functionality # ============================================================================= class TestDevToolsFunctionality: """Test DevTools-specific requirements.""" def test_console_handler(self, mcp_server_content): """Verify console message handler exists.""" assert "async def _on_console" in mcp_server_content def test_request_handler(self, mcp_server_content): """Verify network request handler exists.""" assert "async def _on_request" in mcp_server_content def test_get_active_page_helper(self, mcp_server_content): """Verify _get_active_page helper exists.""" assert "def _get_active_page" in mcp_server_content def test_cdp_connection(self, mcp_server_content): """Verify CDP connection method is used.""" assert "connect_over_cdp" in mcp_server_content def test_playwright_launch(self, mcp_server_content): """Verify Playwright launch for headless mode.""" assert "chromium.launch" in mcp_server_content assert "--no-sandbox" in mcp_server_content # Required for Docker # ============================================================================= # RUN TESTS # ============================================================================= if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])