Skip to main content
Glama

mcp-nixos

by utensils
test_mcp_tools.py14.4 kB
#!/usr/bin/env python3 """Comprehensive tests for all MCP NixOS tools to identify and fix issues.""" from unittest.mock import MagicMock, patch import pytest from mcp_nixos import server def get_tool_function(tool_name: str): """Get the underlying function from a FastMCP tool.""" tool = getattr(server, tool_name) if hasattr(tool, "fn"): return tool.fn return tool # Get the underlying functions for direct use darwin_stats = get_tool_function("darwin_stats") home_manager_info = get_tool_function("home_manager_info") home_manager_list_options = get_tool_function("home_manager_list_options") home_manager_search = get_tool_function("home_manager_search") home_manager_stats = get_tool_function("home_manager_stats") nixos_info = get_tool_function("nixos_info") nixos_search = get_tool_function("nixos_search") class TestNixOSSearchIssues: """Test issues with nixos_search specifically for options.""" @patch("mcp_nixos.server.es_query") @pytest.mark.asyncio async def test_nixos_search_options_now_returns_relevant_results(self, mock_es): """Test that searching for 'services.nginx' returns relevant nginx options.""" # Mock proper nginx-related results mock_es.return_value = [ { "_source": { "option_name": "services.nginx.enable", "option_type": "boolean", "option_description": "Whether to enable Nginx Web Server.", } }, { "_source": { "option_name": "services.nginx.package", "option_type": "package", "option_description": "Nginx package to use.", } }, ] result = await nixos_search("services.nginx", search_type="options", limit=2, channel="stable") # After fix, should return nginx-related options assert "services.nginx.enable" in result assert "services.nginx.package" in result # Should mention the search term assert "services.nginx" in result @patch("mcp_nixos.server.es_query") @pytest.mark.asyncio async def test_nixos_info_option_not_found(self, mock_es): """Test that nixos_info fails to find specific options like services.nginx.enable.""" mock_es.return_value = [] # Empty results result = await nixos_info("services.nginx.enable", type="option", channel="stable") assert "Error (NOT_FOUND)" in result assert "services.nginx.enable" in result class TestHomeManagerIssues: """Test issues with Home Manager tools.""" @patch("mcp_nixos.server.parse_html_options") @pytest.mark.asyncio async def test_home_manager_list_options_incomplete(self, mock_parse): """Test that home_manager_list_options only returns 2 categories (incomplete).""" # Mock returns only 2 categories as seen in the issue mock_parse.return_value = [ {"name": "_module.args", "description": "", "type": ""}, {"name": "accounts.calendar.basePath", "description": "", "type": ""}, {"name": "accounts.email.enable", "description": "", "type": ""}, ] result = await home_manager_list_options() assert "_module (1 options)" in result assert "accounts (2 options)" in result assert "programs" not in result # Missing many categories! # Should have many more categories assert "(2 total)" in result # Only 2 categories found @patch("mcp_nixos.server.parse_html_options") @pytest.mark.asyncio async def test_home_manager_stats_placeholder(self, mock_parse): """Test that home_manager_stats returns actual statistics.""" # Mock parsed options mock_parse.return_value = [ {"name": "programs.git.enable", "type": "boolean", "description": "Enable git"}, {"name": "programs.zsh.enable", "type": "boolean", "description": "Enable zsh"}, {"name": "services.gpg-agent.enable", "type": "boolean", "description": "Enable GPG agent"}, {"name": "home.packages", "type": "list", "description": "Packages to install"}, {"name": "wayland.windowManager.sway.enable", "type": "boolean", "description": "Enable Sway"}, {"name": "xsession.enable", "type": "boolean", "description": "Enable X session"}, ] result = await home_manager_stats() assert "Home Manager Statistics:" in result assert "Total options: 6" in result assert "Categories: 5" in result assert "Top categories:" in result assert "programs: 2 options" in result assert "services: 1 options" in result class TestDarwinIssues: """Test issues with nix-darwin tools.""" @patch("mcp_nixos.server.parse_html_options") @pytest.mark.asyncio async def test_darwin_stats_placeholder(self, mock_parse): """Test that darwin_stats returns actual statistics.""" # Mock parsed options mock_parse.return_value = [ {"name": "services.nix-daemon.enable", "type": "boolean", "description": "Enable nix-daemon"}, {"name": "system.defaults.dock.autohide", "type": "boolean", "description": "Auto-hide dock"}, {"name": "launchd.agents.test", "type": "attribute set", "description": "Launchd agents"}, {"name": "programs.zsh.enable", "type": "boolean", "description": "Enable zsh"}, {"name": "homebrew.enable", "type": "boolean", "description": "Enable Homebrew"}, ] result = await darwin_stats() assert "nix-darwin Statistics:" in result assert "Total options: 5" in result assert "Categories: 5" in result assert "Top categories:" in result assert "services: 1 options" in result assert "system: 1 options" in result class TestHTMLParsingIssues: """Test issues with HTML parsing that affect both Home Manager and Darwin.""" @patch("mcp_nixos.server.requests.get") @pytest.mark.asyncio async def test_parse_html_options_type_extraction(self, mock_get): """Test that type information is not properly extracted from HTML.""" # Mock HTML response with proper structure mock_response = MagicMock() mock_response.text = """ <html> <body> <dt>programs.git.enable</dt> <dd> <p>Whether to enable Git.</p> <span class="term">Type: boolean</span> </dd> <dt>programs.git.package</dt> <dd> <p>The git package to use.</p> <span class="term">Type: package</span> </dd> </body> </html> """ mock_response.raise_for_status = MagicMock() mock_get.return_value = mock_response result = await home_manager_info("programs.git.enable") # Check if type info is properly extracted assert "Type:" in result or "boolean" in result if "Type:" not in result: # Type extraction is failing raise AssertionError("Type information not extracted from HTML") class TestElasticsearchQueryIssues: """Test issues with Elasticsearch query construction.""" @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_es_query_field_names(self, mock_post): """Test that ES queries use correct field names.""" # Mock successful response mock_response = MagicMock() mock_response.json.return_value = {"hits": {"hits": []}} mock_response.raise_for_status = MagicMock() mock_post.return_value = mock_response # Test options search await nixos_search("nginx", search_type="options", limit=1) # Check the query sent to ES call_args = mock_post.call_args query_data = call_args[1]["json"]["query"] # Verify correct field names are used should_clauses = query_data["bool"]["should"] field_names = [] for clause in should_clauses: if "match" in clause: field_names.extend(clause["match"].keys()) elif "wildcard" in clause: field_names.extend(clause["wildcard"].keys()) # After fix, we use wildcard for option_name assert "option_name" in field_names or any("option_name" in str(clause) for clause in should_clauses) assert "option_description" in field_names # Test exact match for nixos_info mock_post.reset_mock() await nixos_info("services.nginx.enable", type="option") call_args = mock_post.call_args query_data = call_args[1]["json"]["query"] # Check for keyword field usage must_clauses = query_data["bool"]["must"] for clause in must_clauses: if "term" in clause and "option_name" in str(clause): # Should use keyword field for exact match assert "option_name.keyword" in str(clause) or "option_name" in str(clause) class TestPlainTextFormatting: """Test that all outputs are plain text without XML/HTML artifacts.""" @patch("mcp_nixos.server.es_query") @pytest.mark.asyncio async def test_nixos_search_strips_html(self, mock_es): """Test that HTML tags in descriptions are properly handled.""" mock_es.return_value = [ { "_source": { "option_name": "test.option", "option_type": "boolean", "option_description": ( "<rendered-html><p>Test description with <code>code</code></p></rendered-html>" ), } } ] result = await nixos_search("test", search_type="options") # Should not contain HTML tags assert "<rendered-html>" not in result assert "</p>" not in result assert "<code>" not in result # But should contain the actual text assert "Test description" in result class TestErrorHandling: """Test error handling across all tools.""" @pytest.mark.asyncio async def test_nixos_search_invalid_parameters(self): """Test parameter validation in nixos_search.""" # Invalid type result = await nixos_search("test", search_type="invalid") assert "Error" in result assert "Invalid type" in result # Invalid channel result = await nixos_search("test", channel="invalid") assert "Error" in result assert "Invalid channel" in result # Invalid limit result = await nixos_search("test", limit=0) assert "Error" in result assert "Limit must be 1-100" in result @patch("mcp_nixos.server.requests.get") @pytest.mark.asyncio async def test_network_error_handling(self, mock_get): """Test handling of network errors.""" mock_get.side_effect = Exception("Network error") result = await home_manager_search("test") assert "Error" in result assert "Failed to fetch docs" in result or "Network error" in result class TestRealAPIBehavior: """Tests that verify actual API behavior (can be skipped in CI).""" @pytest.mark.integration @pytest.mark.asyncio async def test_real_nixos_option_search(self): """Test real NixOS API option search behavior.""" # This would make actual API calls to verify the issue result = await nixos_search("services.nginx.enable", search_type="options", channel="stable") # The search should return nginx-related options, not random ones if "appstream.enable" in result: pytest.fail("Search returns unrelated options - API query issue confirmed") @pytest.mark.integration @pytest.mark.asyncio async def test_real_home_manager_parsing(self): """Test real Home Manager HTML parsing.""" result = await home_manager_list_options() # Should have many categories, not just 2 if "(2 total)" in result: pytest.fail("Only 2 categories found - HTML parsing issue confirmed") # Additional test utilities def count_lines(text: str) -> int: """Count non-empty lines in output.""" return len([line for line in text.split("\n") if line.strip()]) def has_plain_text_format(text: str) -> bool: """Check if text follows plain text format without XML/HTML.""" forbidden_patterns = [ "<rendered-html>", "</rendered-html>", "<p>", "</p>", "<code>", "</code>", "<a ", "</a>", "<?xml", ] return not any(pattern in text for pattern in forbidden_patterns) class TestOutputFormat: """Test output formatting consistency.""" @patch("mcp_nixos.server.es_query") @pytest.mark.asyncio async def test_search_result_format(self, mock_es): """Test consistent formatting of search results.""" mock_es.return_value = [ { "_source": { "package_pname": "nginx", "package_pversion": "1.24.0", "package_description": "A web server", } } ] result = await nixos_search("nginx", search_type="packages", limit=1) # Check format assert "Found 1 packages matching" in result assert "• nginx (1.24.0)" in result assert " A web server" in result # Indented description # Check plain text assert has_plain_text_format(result) @patch("mcp_nixos.server.parse_html_options") @pytest.mark.asyncio async def test_home_manager_format_consistency(self, mock_parse): """Test Home Manager output format consistency.""" mock_parse.return_value = [ {"name": "programs.git.enable", "description": "Whether to enable Git.", "type": "boolean"} ] result = await home_manager_search("git", limit=1) # Check format matches nixos_search style assert "Found 1 Home Manager options matching" in result assert "• programs.git.enable" in result assert " Type: boolean" in result assert " Whether to enable Git." in result if __name__ == "__main__": pytest.main([__file__, "-v"])

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/utensils/mcp-nixos'

If you have feedback or need assistance with the MCP directory API, please join our Discord server