Skip to main content
Glama

mcp-nixos

by utensils
test_channels.py27.1 kB
#!/usr/bin/env python3 """Tests for robust channel handling functionality.""" from unittest.mock import Mock, patch import pytest import requests from mcp_nixos import server from mcp_nixos.server import ( channel_cache, get_channel_suggestions, get_channels, validate_channel, ) 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_info = get_tool_function("nixos_info") nixos_search = get_tool_function("nixos_search") nixos_stats = get_tool_function("nixos_stats") class TestChannelHandling: """Test robust channel handling functionality.""" @patch("requests.post") def test_discover_available_channels_success(self, mock_post): """Test successful channel discovery.""" # Mock successful responses for some channels mock_responses = { "latest-43-nixos-unstable": {"count": 151798}, "latest-43-nixos-25.05": {"count": 151698}, "latest-43-nixos-24.11": {"count": 142034}, } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in mock_responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp # Default to 404 for unknown patterns mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect # Clear cache first channel_cache.available_channels = None result = channel_cache.get_available() assert "latest-43-nixos-unstable" in result assert "latest-43-nixos-25.05" in result assert "latest-43-nixos-24.11" in result assert "151,798 documents" in result["latest-43-nixos-unstable"] @patch("requests.post") def test_discover_available_channels_with_cache(self, mock_post): """Test that channel discovery uses cache.""" # Set up cache channel_cache.available_channels = {"test": "cached"} result = channel_cache.get_available() # Should return cached result without making API calls assert result == {"test": "cached"} mock_post.assert_not_called() @patch("mcp_nixos.server.get_channels") @patch("requests.post") def test_validate_channel_success(self, mock_post, mock_get_channels): """Test successful channel validation.""" mock_get_channels.return_value = {"stable": "latest-43-nixos-25.05"} mock_resp = Mock() mock_resp.status_code = 200 mock_resp.json.return_value = {"count": 100000} mock_post.return_value = mock_resp result = validate_channel("stable") assert result is True @patch("mcp_nixos.server.get_channels") def test_validate_channel_failure(self, mock_get_channels): """Test channel validation failure.""" mock_get_channels.return_value = {"stable": "latest-43-nixos-25.05"} result = validate_channel("nonexistent") assert result is False def test_validate_channel_invalid_name(self): """Test validation of channel not in CHANNELS.""" result = validate_channel("totally-invalid") assert result is False @patch("mcp_nixos.server.get_channels") def test_get_channel_suggestions_similar(self, mock_get_channels): """Test getting suggestions for similar channel names.""" # Mock the available channels mock_get_channels.return_value = { "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", } result = get_channel_suggestions("unstabl") assert "unstable" in result result = get_channel_suggestions("24") assert "24.11" in result @patch("mcp_nixos.server.get_channels") def test_get_channel_suggestions_fallback(self, mock_get_channels): """Test fallback suggestions for completely invalid names.""" # Mock the available channels mock_get_channels.return_value = { "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", } result = get_channel_suggestions("totally-random-xyz") assert "unstable" in result assert "stable" in result assert "25.05" in result @patch("mcp_nixos.server.channel_cache.get_available") @patch("mcp_nixos.server.channel_cache.get_resolved") @pytest.mark.asyncio async def test_nixos_channels_tool(self, mock_resolved, mock_discover): """Test nixos_channels tool output.""" mock_discover.return_value = { "latest-43-nixos-unstable": "151,798 documents", "latest-43-nixos-25.05": "151,698 documents", "latest-43-nixos-24.11": "142,034 documents", } mock_resolved.return_value = { "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", } result = await nixos_channels() assert "NixOS Channels" in result # Match both old and new format assert "unstable → latest-43-nixos-unstable" in result or "unstable \u2192 latest-43-nixos-unstable" in result assert "stable" in result and "latest-43-nixos-25.05" in result assert "✓ Available" in result assert "151,798 documents" in result @patch("mcp_nixos.server.channel_cache.get_available") @patch("mcp_nixos.server.channel_cache.get_resolved") @pytest.mark.asyncio async def test_nixos_channels_with_unavailable(self, mock_resolved, mock_discover): """Test nixos_channels tool with some unavailable channels.""" # Only return some channels as available mock_discover.return_value = {"latest-43-nixos-unstable": "151,798 documents"} mock_resolved.return_value = { "unstable": "latest-43-nixos-unstable", "stable": "latest-43-nixos-25.05", # Not available "25.05": "latest-43-nixos-25.05", } result = await nixos_channels() assert "✓ Available" in result assert "✗ Unavailable" in result @patch("mcp_nixos.server.channel_cache.get_available") @pytest.mark.asyncio async def test_nixos_channels_with_extra_discovered(self, mock_discover): """Test nixos_channels with extra discovered channels.""" mock_discover.return_value = { "latest-43-nixos-unstable": "151,798 documents", "latest-43-nixos-25.05": "151,698 documents", "latest-44-nixos-unstable": "152,000 documents", # New channel } result = await nixos_channels() assert "Additional available channels:" in result assert "latest-44-nixos-unstable" in result @pytest.mark.asyncio async def test_nixos_stats_with_invalid_channel(self): """Test nixos_stats with invalid channel shows suggestions.""" result = await nixos_stats("invalid-channel") assert "Error (ERROR):" in result assert "Invalid channel 'invalid-channel'" in result assert "Available channels:" in result @pytest.mark.asyncio async def test_nixos_search_with_invalid_channel(self): """Test nixos_search with invalid channel shows suggestions.""" result = await nixos_search("test", channel="invalid-channel") assert "Error (ERROR):" in result assert "Invalid channel 'invalid-channel'" in result assert "Available channels:" in result @patch("mcp_nixos.server.channel_cache.get_resolved") def test_channel_mappings_dynamic(self, mock_resolved): """Test that dynamic channel mappings work correctly.""" # Mock the resolved channels mock_resolved.return_value = { "stable": "latest-43-nixos-25.05", "unstable": "latest-43-nixos-unstable", "25.05": "latest-43-nixos-25.05", "24.11": "latest-43-nixos-24.11", "beta": "latest-43-nixos-25.05", } channels = get_channels() # Should have basic channels assert "stable" in channels assert "unstable" in channels # Stable should point to a valid channel index assert channels["stable"].startswith("latest-") assert "nixos" in channels["stable"] # Unstable should point to unstable index assert "unstable" in channels["unstable"] @patch("requests.post") def test_discover_channels_handles_exceptions(self, mock_post): """Test channel discovery handles network exceptions gracefully.""" mock_post.side_effect = requests.ConnectionError("Network error") # Clear cache channel_cache.available_channels = None result = channel_cache.get_available() # Should return empty dict when all requests fail assert result == {} @patch("requests.post") def test_validate_channel_handles_exceptions(self, mock_post): """Test channel validation handles exceptions gracefully.""" mock_post.side_effect = requests.ConnectionError("Network error") result = validate_channel("stable") assert result is False @patch("mcp_nixos.server.channel_cache.get_available") @pytest.mark.asyncio async def test_nixos_channels_handles_exceptions(self, mock_discover): """Test nixos_channels tool handles exceptions gracefully.""" mock_discover.side_effect = Exception("Discovery failed") result = await nixos_channels() assert "Error (ERROR):" in result assert "Discovery failed" in result @patch("mcp_nixos.server.get_channels") def test_channel_suggestions_for_legacy_channels(self, mock_get_channels): """Test suggestions work for legacy channel references.""" mock_get_channels.return_value = { "stable": "latest-43-nixos-25.05", "unstable": "latest-43-nixos-unstable", "25.05": "latest-43-nixos-25.05", "24.11": "latest-43-nixos-24.11", "beta": "latest-43-nixos-25.05", } # Test old stable reference result = get_channel_suggestions("20.09") assert "24.11" in result or "stable" in result # Test partial version result = get_channel_suggestions("25") assert "25.05" in result @patch("requests.post") def test_discover_channels_filters_empty_indices(self, mock_post): """Test that discovery filters out indices with 0 documents.""" def side_effect(url, **kwargs): mock_resp = Mock() if "empty-index" in url: mock_resp.status_code = 200 mock_resp.json.return_value = {"count": 0} # Empty index else: mock_resp.status_code = 200 mock_resp.json.return_value = {"count": 100000} return mock_resp mock_post.side_effect = side_effect # Clear cache channel_cache.available_channels = None # This should work with the actual test patterns result = channel_cache.get_available() # Should not include any indices with 0 documents for _, info in result.items(): # Check that it doesn't start with "0 documents" assert not info.startswith("0 documents") # ===== Content from test_dynamic_channels.py ===== class TestDynamicChannelLifecycle: """Test dynamic channel detection and lifecycle management.""" def setup_method(self): """Clear caches before each test.""" channel_cache.available_channels = None channel_cache.resolved_channels = None @patch("requests.post") def test_channel_discovery_future_proof(self, mock_post): """Test discovery works with future NixOS releases.""" # Simulate future release state future_responses = { "latest-44-nixos-unstable": {"count": 160000}, "latest-44-nixos-25.11": {"count": 155000}, # New stable "latest-44-nixos-25.05": {"count": 152000}, # Old stable "latest-43-nixos-25.05": {"count": 151000}, # Legacy } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in future_responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect # Test discovery available = channel_cache.get_available() assert "latest-44-nixos-unstable" in available assert "latest-44-nixos-25.11" in available # Test resolution - should pick 25.11 as new stable channels = channel_cache.get_resolved() assert channels["stable"] == "latest-44-nixos-25.11" assert channels["unstable"] == "latest-44-nixos-unstable" assert channels["25.11"] == "latest-44-nixos-25.11" assert channels["25.05"] == "latest-44-nixos-25.05" @patch("requests.post") def test_stable_detection_by_version_priority(self, mock_post): """Test stable detection prioritizes higher version numbers.""" # Same generation, different versions responses = { "latest-43-nixos-24.11": {"count": 150000}, "latest-43-nixos-25.05": {"count": 140000}, # Lower count but higher version "latest-43-nixos-unstable": {"count": 155000}, } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect channels = channel_cache.get_resolved() # Should pick 25.05 despite lower count (higher version) assert channels["stable"] == "latest-43-nixos-25.05" @patch("requests.post") def test_stable_detection_by_count_when_same_version(self, mock_post): """Test stable detection uses count as tiebreaker.""" responses = { "latest-43-nixos-25.05": {"count": 150000}, "latest-44-nixos-25.05": {"count": 155000}, # Higher count, same version "latest-43-nixos-unstable": {"count": 160000}, } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect channels = channel_cache.get_resolved() # Should pick higher count for same version assert channels["stable"] == "latest-44-nixos-25.05" @patch("requests.post") def test_channel_discovery_handles_no_channels(self, mock_post): """Test graceful handling when no channels are available.""" mock_post.return_value = Mock(status_code=404) available = channel_cache.get_available() assert available == {} channels = channel_cache.get_resolved() assert channels == {} @patch("requests.post") def test_channel_discovery_partial_availability(self, mock_post): """Test handling when only some channels are available.""" responses = { "latest-43-nixos-unstable": {"count": 150000}, # No stable releases available } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect channels = channel_cache.get_resolved() assert channels["unstable"] == "latest-43-nixos-unstable" assert "stable" not in channels # No stable release found @patch("mcp_nixos.server.channel_cache.get_resolved") @pytest.mark.asyncio async def test_nixos_stats_with_dynamic_channels(self, mock_resolve): """Test nixos_stats works with dynamically resolved channels.""" mock_resolve.return_value = { "stable": "latest-44-nixos-25.11", "unstable": "latest-44-nixos-unstable", } with patch("requests.post") as mock_post: # Mock successful response mock_resp = Mock() mock_resp.status_code = 200 mock_resp.json.return_value = {"count": 1000} mock_resp.raise_for_status.return_value = None mock_post.return_value = mock_resp # Should work with new stable result = await nixos_stats("stable") # Should not error and should contain statistics assert "NixOS Statistics" in result assert "stable" in result # Should have made API calls assert mock_post.called @patch("mcp_nixos.server.channel_cache.get_resolved") @pytest.mark.asyncio async def test_nixos_search_with_dynamic_channels(self, mock_resolve): """Test nixos_search works with dynamically resolved channels.""" mock_resolve.return_value = { "stable": "latest-44-nixos-25.11", "unstable": "latest-44-nixos-unstable", } with patch("mcp_nixos.server.es_query") as mock_es: mock_es.return_value = [] result = await nixos_search("test", channel="stable") assert "No packages found" in result @patch("mcp_nixos.server.channel_cache.get_available") @pytest.mark.asyncio async def test_nixos_channels_tool_shows_current_stable(self, mock_discover): """Test nixos_channels tool clearly shows current stable version.""" mock_discover.return_value = { "latest-44-nixos-25.11": "155,000 documents", "latest-44-nixos-unstable": "160,000 documents", } with patch("mcp_nixos.server.channel_cache.get_resolved") as mock_resolve: mock_resolve.return_value = { "stable": "latest-44-nixos-25.11", "25.11": "latest-44-nixos-25.11", "unstable": "latest-44-nixos-unstable", } result = await nixos_channels() assert "stable (current: 25.11)" in result assert "latest-44-nixos-25.11" in result assert "dynamically discovered" in result @pytest.mark.asyncio async def test_channel_suggestions_work_with_dynamic_channels(self): """Test channel suggestions work with dynamic resolution.""" with patch("mcp_nixos.server.get_channels") as mock_get: mock_get.return_value = { "stable": "latest-44-nixos-25.11", "unstable": "latest-44-nixos-unstable", "25.11": "latest-44-nixos-25.11", } result = await nixos_stats("invalid-channel") assert "Available channels:" in result assert any(ch in result for ch in ["stable", "unstable"]) @patch("requests.post") def test_caching_behavior(self, mock_post): """Test that caching works correctly.""" responses = { "latest-43-nixos-unstable": {"count": 150000}, "latest-43-nixos-25.05": {"count": 145000}, } call_count = 0 def side_effect(url, **kwargs): nonlocal call_count call_count += 1 mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect # First call should hit API channels1 = get_channels() first_call_count = call_count # Second call should use cache channels2 = get_channels() second_call_count = call_count assert channels1 == channels2 assert second_call_count == first_call_count # No additional API calls @patch("requests.post") def test_malformed_version_handling(self, mock_post): """Test handling of malformed version numbers.""" responses = { "latest-43-nixos-unstable": {"count": 150000}, "latest-43-nixos-badversion": {"count": 145000}, # Invalid version "latest-43-nixos-25.05": {"count": 140000}, # Valid version } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect channels = channel_cache.get_resolved() # Should ignore malformed version and use valid one assert channels["stable"] == "latest-43-nixos-25.05" assert "badversion" not in channels @patch("requests.post") def test_network_error_handling(self, mock_post): """Test handling of network errors during discovery.""" mock_post.side_effect = requests.ConnectionError("Network error") available = channel_cache.get_available() assert available == {} channels = channel_cache.get_resolved() assert channels == {} @patch("requests.post") def test_zero_document_filtering(self, mock_post): """Test that channels with zero documents are filtered out.""" responses = { "latest-43-nixos-unstable": {"count": 150000}, "latest-43-nixos-25.05": {"count": 0}, # Empty index "latest-43-nixos-24.11": {"count": 140000}, } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect available = channel_cache.get_available() assert "latest-43-nixos-unstable" in available assert "latest-43-nixos-25.05" not in available # Filtered out assert "latest-43-nixos-24.11" in available @patch("requests.post") def test_version_comparison_edge_cases(self, mock_post): """Test version comparison with edge cases.""" responses = { "latest-43-nixos-unstable": {"count": 150000}, "latest-43-nixos-20.09": {"count": 100000}, # Old version "latest-43-nixos-25.05": {"count": 145000}, # Current "latest-43-nixos-30.05": {"count": 140000}, # Future } def side_effect(url, **kwargs): mock_resp = Mock() for pattern, response in responses.items(): if pattern in url: mock_resp.status_code = 200 mock_resp.json.return_value = response return mock_resp mock_resp.status_code = 404 return mock_resp mock_post.side_effect = side_effect channels = channel_cache.get_resolved() # Should pick highest version (30.05) assert channels["stable"] == "latest-43-nixos-30.05" assert "20.09" in channels # Old versions still mapped assert "25.05" in channels assert "30.05" in channels @patch("mcp_nixos.server.channel_cache.get_available") def test_beta_alias_behavior(self, mock_discover): """Test that beta is always an alias for stable.""" mock_discover.return_value = { "latest-44-nixos-25.11": "155,000 documents", "latest-44-nixos-unstable": "160,000 documents", } channels = channel_cache.get_resolved() assert "beta" in channels assert channels["beta"] == channels["stable"] @pytest.mark.asyncio async def test_integration_with_all_tools(self): """Test that all tools work with dynamic channels.""" with patch("mcp_nixos.server.get_channels") as mock_get: mock_get.return_value = { "stable": "latest-44-nixos-25.11", "unstable": "latest-44-nixos-unstable", } with patch("mcp_nixos.server.es_query") as mock_es: mock_es.return_value = [] with patch("requests.post") as mock_post: # Mock successful response for nixos_stats mock_resp = Mock() mock_resp.status_code = 200 mock_resp.json.return_value = {"count": 1000} mock_resp.raise_for_status.return_value = None mock_post.return_value = mock_resp # Test all tools that use channels tools_to_test = [ lambda: nixos_search("test", channel="stable"), lambda: nixos_info("test", channel="stable"), lambda: nixos_stats("stable"), ] for tool in tools_to_test: result = await tool() # Should not error due to channel resolution assert ( "Error" not in result or "not found" in result or "No packages found" in result or "NixOS Statistics" in result )

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