Skip to main content
Glama

mcp-nixos

by utensils
test_nixos_stats.py17.6 kB
"""Regression test for NixOS stats to ensure correct field names are used.""" from unittest.mock import Mock, 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 nixos_channels = get_tool_function("nixos_channels") nixos_stats = get_tool_function("nixos_stats") def setup_channel_mocks(mock_cache, mock_validate, channels=None): """Setup channel mocks with default or custom channels.""" if channels is None: channels = { "unstable": "latest-43-nixos-unstable", "stable": "latest-43-nixos-25.05", "25.05": "latest-43-nixos-25.05", "24.11": "latest-43-nixos-24.11", "beta": "latest-43-nixos-25.05", } mock_cache.get_available.return_value = {v: f"{v.split('-')[-1]} docs" for v in channels.values() if v} mock_cache.get_resolved.return_value = channels mock_validate.side_effect = lambda channel: channel in channels class TestNixOSStatsRegression: """Ensure NixOS stats uses correct field names in queries.""" @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_nixos_stats_uses_correct_query_fields(self, mock_post, mock_cache, mock_validate): """Test that stats uses 'type' field with term query, not 'package'/'option' with exists query.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock responses pkg_resp = Mock() pkg_resp.json.return_value = {"count": 129865} opt_resp = Mock() opt_resp.json.return_value = {"count": 21933} mock_post.side_effect = [pkg_resp, opt_resp] # Call the function result = await nixos_stats() # Verify the function returns expected output assert "NixOS Statistics for unstable channel:" in result assert "• Packages: 129,865" in result assert "• Options: 21,933" in result # Verify the correct queries were sent assert mock_post.call_count == 2 # Check package count query pkg_call = mock_post.call_args_list[0] assert pkg_call[1]["json"]["query"] == {"term": {"type": "package"}} # Check option count query opt_call = mock_post.call_args_list[1] assert opt_call[1]["json"]["query"] == {"term": {"type": "option"}} @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_nixos_stats_handles_zero_counts(self, mock_post, mock_cache, mock_validate): """Test that stats correctly handles zero counts.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock responses with zero counts mock_resp = Mock() mock_resp.json.return_value = {"count": 0} mock_post.return_value = mock_resp result = await nixos_stats() # Should return error when both counts are zero (our improved logic) assert "Error (ERROR): Failed to retrieve statistics" in result @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_nixos_stats_all_channels(self, mock_post, mock_cache, mock_validate): """Test that stats works for all defined channels.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock responses mock_resp = Mock() mock_resp.json.return_value = {"count": 12345} mock_post.return_value = mock_resp # Test with known channels for channel in ["stable", "unstable"]: result = await nixos_stats(channel=channel) assert f"NixOS Statistics for {channel} channel:" in result assert "• Packages: 12,345" in result assert "• Options: 12,345" in result # ===== Content from test_package_counts_eval.py ===== class TestPackageCountsEval: """Test evaluations for getting package counts per NixOS channel.""" @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_get_package_counts_per_channel(self, mock_post, mock_cache, mock_validate): """Eval: User wants package counts for each NixOS channel.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock channel discovery responses mock_count_responses = { "latest-43-nixos-unstable": {"count": 151798}, "latest-43-nixos-25.05": {"count": 151698}, "latest-43-nixos-24.11": {"count": 142034}, } # Mock stats responses for each channel mock_stats_responses = { "unstable": { "aggregations": { "attr_count": {"value": 151798}, "option_count": {"value": 20156}, "program_count": {"value": 3421}, "license_count": {"value": 125}, "maintainer_count": {"value": 3254}, "platform_counts": { "buckets": [ {"key": "x86_64-linux", "doc_count": 145234}, {"key": "aarch64-linux", "doc_count": 142123}, {"key": "x86_64-darwin", "doc_count": 98765}, {"key": "aarch64-darwin", "doc_count": 97654}, ] }, } }, "25.05": { "aggregations": { "attr_count": {"value": 151698}, "option_count": {"value": 20145}, "program_count": {"value": 3420}, "license_count": {"value": 125}, "maintainer_count": {"value": 3250}, "platform_counts": { "buckets": [ {"key": "x86_64-linux", "doc_count": 145134}, {"key": "aarch64-linux", "doc_count": 142023}, {"key": "x86_64-darwin", "doc_count": 98665}, {"key": "aarch64-darwin", "doc_count": 97554}, ] }, } }, "24.11": { "aggregations": { "attr_count": {"value": 142034}, "option_count": {"value": 19876}, "program_count": {"value": 3200}, "license_count": {"value": 123}, "maintainer_count": {"value": 3100}, "platform_counts": { "buckets": [ {"key": "x86_64-linux", "doc_count": 138000}, {"key": "aarch64-linux", "doc_count": 135000}, {"key": "x86_64-darwin", "doc_count": 92000}, {"key": "aarch64-darwin", "doc_count": 91000}, ] }, } }, } def side_effect(*args, **kwargs): url = args[0] # Handle count requests for channel discovery if "/_count" in url: for index, count_data in mock_count_responses.items(): if index in url: mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock(return_value=count_data) mock_response.raise_for_status = Mock() return mock_response # Not found mock_response = Mock() mock_response.status_code = 404 mock_response.raise_for_status = Mock(side_effect=Exception("Not found")) return mock_response # Handle stats count requests (with type filter) json_data = kwargs.get("json", {}) query = json_data.get("query", {}) # Determine which channel from URL for channel, index in [ ("unstable", "latest-43-nixos-unstable"), ("25.05", "latest-43-nixos-25.05"), ("24.11", "latest-43-nixos-24.11"), ]: if index in url: stats = mock_stats_responses.get(channel, mock_stats_responses["unstable"]) mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() # Check if it's a package or option count if query.get("term", {}).get("type") == "package": mock_response.json = Mock(return_value={"count": stats["aggregations"]["attr_count"]["value"]}) elif query.get("term", {}).get("type") == "option": mock_response.json = Mock( return_value={"count": stats["aggregations"]["option_count"]["value"]} ) else: # General count mock_response.json = Mock(return_value={"count": stats["aggregations"]["attr_count"]["value"]}) return mock_response # Default response - return a proper mock mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() mock_response.json = Mock(return_value={"count": 151798}) return mock_response mock_post.side_effect = side_effect # Step 1: Get available channels channels_result = await nixos_channels() assert "24.11" in channels_result assert "25.05" in channels_result assert "unstable" in channels_result # Check that document counts are present (don't hardcode exact values as they change) assert "docs)" in channels_result assert "Available" in channels_result # Step 2: Get stats for each channel stats_unstable = await nixos_stats("unstable") assert "Packages:" in stats_unstable assert "Options:" in stats_unstable stats_stable = await nixos_stats("stable") # Should resolve to 25.05 assert "Packages:" in stats_stable stats_24_11 = await nixos_stats("24.11") assert "Packages:" in stats_24_11 # Verify package count differences # unstable should have the most packages # 25.05 (current stable) should be close to unstable # 24.11 should have fewer packages @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_package_counts_with_beta_alias(self, mock_post, mock_cache, mock_validate): """Eval: User asks about beta channel package count.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock responses for channel discovery mock_count_response = Mock() mock_count_response.status_code = 200 mock_count_response.json.return_value = {"count": 151698} mock_stats_response = Mock() mock_stats_response.json.return_value = { "aggregations": { "attr_count": {"value": 151698}, "option_count": {"value": 20145}, "program_count": {"value": 3420}, "license_count": {"value": 125}, "maintainer_count": {"value": 3250}, "platform_counts": { "buckets": [ {"key": "x86_64-linux", "doc_count": 145134}, ] }, } } def side_effect(*args, **kwargs): url = args[0] if "/_count" in url and "25.05" in url: return mock_count_response if "/_count" in url: # Other channels not found mock_404 = Mock() mock_404.status_code = 404 return mock_404 # Stats request json_data = kwargs.get("json", {}) query = json_data.get("query", {}) mock_response = Mock() mock_response.status_code = 200 # Check if it's a package or option count if query.get("term", {}).get("type") == "package": mock_response.json.return_value = {"count": 151698} elif query.get("term", {}).get("type") == "option": mock_response.json.return_value = {"count": 20145} else: # General count mock_response.json.return_value = {"count": 151698} return mock_response mock_post.side_effect = side_effect # Beta should resolve to stable (25.05) result = await nixos_stats("beta") assert "Packages:" in result assert "beta" in result @patch("mcp_nixos.server.validate_channel") @patch("mcp_nixos.server.channel_cache") @patch("mcp_nixos.server.requests.post") @pytest.mark.asyncio async def test_compare_package_counts_across_channels(self, mock_post, mock_cache, mock_validate): """Eval: User wants to compare package growth across releases.""" # Setup channel mocks setup_channel_mocks(mock_cache, mock_validate) # Mock responses with increasing package counts mock_count_responses = { "latest-43-nixos-unstable": {"count": 151798}, "latest-43-nixos-25.05": {"count": 151698}, "latest-43-nixos-24.11": {"count": 142034}, "latest-43-nixos-24.05": {"count": 135000}, } channel_stats = { "24.05": 135000, "24.11": 142034, "25.05": 151698, "unstable": 151798, } def side_effect(*args, **kwargs): url = args[0] # Handle count requests for channel discovery if "/_count" in url: for index, count_data in mock_count_responses.items(): if index in url: mock_response = Mock() mock_response.status_code = 200 mock_response.json = Mock(return_value=count_data) mock_response.raise_for_status = Mock() return mock_response # Not found mock_response = Mock() mock_response.status_code = 404 mock_response.raise_for_status = Mock(side_effect=Exception("Not found")) return mock_response # Handle stats count requests (with type filter) json_data = kwargs.get("json", {}) query = json_data.get("query", {}) # Extract channel from URL and return appropriate stats channel_to_index = { "24.05": "latest-43-nixos-24.05", "24.11": "latest-43-nixos-24.11", "25.05": "latest-43-nixos-25.05", "unstable": "latest-43-nixos-unstable", } for channel, count in channel_stats.items(): index = channel_to_index.get(channel) if index and index in url: mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() # Check if it's a package or option count if query.get("term", {}).get("type") == "package": mock_response.json = Mock(return_value={"count": count}) elif query.get("term", {}).get("type") == "option": mock_response.json = Mock(return_value={"count": 20000}) else: # General count mock_response.json = Mock(return_value={"count": count}) return mock_response # Default to unstable mock_response = Mock() mock_response.status_code = 200 mock_response.raise_for_status = Mock() if query.get("term", {}).get("type") == "package": mock_response.json = Mock(return_value={"count": 151798}) elif query.get("term", {}).get("type") == "option": mock_response.json = Mock(return_value={"count": 20156}) else: mock_response.json = Mock(return_value={"count": 151798}) return mock_response mock_post.side_effect = side_effect # Get stats for multiple channels to compare growth # Only use channels that are currently available for channel in ["24.11", "25.05", "unstable"]: stats = await nixos_stats(channel) # Just verify we get stats back with package info assert "Packages:" in stats assert "channel:" in stats.lower() # Check case-insensitively

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