Files
skill-seekers-reference/tests/test_mcp_git_sources.py
yusyus db63e67986 fix: resolve all test failures — 2115 passing, 0 failures
Fixes several categories of test failures to achieve a clean test suite:

**Python 3.14 / chromadb compatibility**
- chroma.py: broaden except clause to catch pydantic ConfigError on Python 3.14
- test_adaptors_e2e.py, test_integration_adaptors.py: skip on (ImportError, Exception)

**sys.modules corruption (test isolation)**
- test_swift_detection.py: save/restore all skill_seekers.cli modules AND parent
  package attributes in test_empty_swift_patterns_handled_gracefully; prevents
  @patch decorators in downstream test files from targeting stale module objects

**Removed unnecessary @unittest.skip decorators**
- test_claude_adaptor.py, test_gemini_adaptor.py, test_openai_adaptor.py: remove
  skip from tests that already had pass-body or were compatible once deps installed

**Fixed openai import guard for installed package**
- test_openai_adaptor.py: use patch.dict(sys.modules, {"openai": None}) for
  test_upload_missing_library since openai is now a transitive dep

**langchain import path update**
- test_rag_chunker.py: fix from langchain.schema → langchain_core.documents

**config_extractor tomllib fallback**
- config_extractor.py: use stdlib tomllib (Python 3.11+) as fallback when
  tomli/toml packages are not installed

**Remove redundant sys.path.insert() calls**
- codebase_scraper.py, doc_scraper.py, enhance_skill.py, enhance_skill_local.py,
  estimate_pages.py, install_skill.py: remove legacy path manipulation no longer
  needed with pip install -e . (src/ layout)

**Test fixes: removed @requires_github from fully-mocked tests**
- test_unified_analyzer.py: 5 tests that mock GitHubThreeStreamFetcher don't
  need a real token; remove decorator so they always run

**macOS-specific test improvements**
- test_terminal_detection.py: use @patch(sys.platform, "darwin") instead of
  runtime skipTest() so tests run on all platforms

**Dependency updates**
- pyproject.toml, uv.lock: add langchain and llama-index as core dependencies

**New workflow presets and tests**
- src/skill_seekers/workflows/: add 60 new domain-specific workflow YAML presets
- tests/test_mcp_workflow_tools.py: tests for MCP workflow tool implementations
- tests/test_unified_scraper_orchestration.py: tests for UnifiedScraper methods

Result: 2115 passed, 158 skipped (external services/long-running), 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 20:43:17 +03:00

568 lines
21 KiB
Python

#!/usr/bin/env python3
"""
MCP Integration Tests for Git Config Sources
Tests the complete MCP tool workflow for git-based config fetching
"""
import json
from unittest.mock import MagicMock, patch
import pytest
# Test if MCP is available
try:
import mcp # noqa: F401
from mcp.types import TextContent
MCP_AVAILABLE = True
except ImportError:
MCP_AVAILABLE = False
TextContent = None # Define placeholder
@pytest.fixture
def temp_dirs(tmp_path):
"""Create temporary directories for testing."""
config_dir = tmp_path / "config"
cache_dir = tmp_path / "cache"
dest_dir = tmp_path / "dest"
config_dir.mkdir()
cache_dir.mkdir()
dest_dir.mkdir()
return {"config": config_dir, "cache": cache_dir, "dest": dest_dir}
@pytest.fixture
def mock_git_repo(temp_dirs):
"""Create a mock git repository with config files."""
repo_path = temp_dirs["cache"] / "test-source"
repo_path.mkdir()
(repo_path / ".git").mkdir()
# Create sample config files
react_config = {
"name": "react",
"description": "React framework",
"base_url": "https://react.dev/",
}
(repo_path / "react.json").write_text(json.dumps(react_config, indent=2))
vue_config = {"name": "vue", "description": "Vue framework", "base_url": "https://vuejs.org/"}
(repo_path / "vue.json").write_text(json.dumps(vue_config, indent=2))
return repo_path
@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP not available")
@pytest.mark.asyncio
class TestFetchConfigModes:
"""Test fetch_config tool with different modes."""
async def test_fetch_config_api_mode_list(self):
"""Test API mode - listing available configs."""
from skill_seekers.mcp.server import fetch_config_tool
with patch("skill_seekers.mcp.tools.source_tools.httpx.AsyncClient") as mock_client:
# Mock API response
mock_response = MagicMock()
mock_response.json.return_value = {
"configs": [
{
"name": "react",
"category": "web-frameworks",
"description": "React framework",
"type": "single",
},
{
"name": "vue",
"category": "web-frameworks",
"description": "Vue framework",
"type": "single",
},
],
"total": 2,
}
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
args = {"list_available": True}
result = await fetch_config_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "react" in result[0].text
assert "vue" in result[0].text
async def test_fetch_config_api_mode_download(self, temp_dirs):
"""Test API mode - downloading specific config."""
from skill_seekers.mcp.server import fetch_config_tool
with patch("skill_seekers.mcp.tools.source_tools.httpx.AsyncClient") as mock_client:
# Mock API responses
mock_detail_response = MagicMock()
mock_detail_response.json.return_value = {
"name": "react",
"category": "web-frameworks",
"description": "React framework",
"download_url": "https://api.skillseekersweb.com/api/configs/react/download",
}
mock_download_response = MagicMock()
mock_download_response.json.return_value = {
"name": "react",
"base_url": "https://react.dev/",
}
mock_client_instance = mock_client.return_value.__aenter__.return_value
mock_client_instance.get.side_effect = [mock_detail_response, mock_download_response]
args = {"config_name": "react", "destination": str(temp_dirs["dest"])}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "react" in result[0].text
# Verify file was created
config_file = temp_dirs["dest"] / "react.json"
assert config_file.exists()
@patch("skill_seekers.mcp.git_repo.GitConfigRepo")
async def test_fetch_config_git_url_mode(self, mock_git_repo_class, temp_dirs):
"""Test Git URL mode - direct git clone."""
from skill_seekers.mcp.server import fetch_config_tool
# Mock GitConfigRepo
mock_repo_instance = MagicMock()
mock_repo_path = temp_dirs["cache"] / "temp_react"
mock_repo_path.mkdir()
# Create mock config file
react_config = {"name": "react", "base_url": "https://react.dev/"}
(mock_repo_path / "react.json").write_text(json.dumps(react_config))
mock_repo_instance.clone_or_pull.return_value = mock_repo_path
mock_repo_instance.get_config.return_value = react_config
mock_git_repo_class.return_value = mock_repo_instance
args = {
"config_name": "react",
"git_url": "https://github.com/myorg/configs.git",
"destination": str(temp_dirs["dest"]),
}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "git URL" in result[0].text
assert "react" in result[0].text
# Verify clone was called
mock_repo_instance.clone_or_pull.assert_called_once()
# Verify file was created
config_file = temp_dirs["dest"] / "react.json"
assert config_file.exists()
@patch("skill_seekers.mcp.git_repo.GitConfigRepo")
@patch("skill_seekers.mcp.source_manager.SourceManager")
async def test_fetch_config_source_mode(
self, mock_source_manager_class, mock_git_repo_class, temp_dirs
):
"""Test Source mode - using named source from registry."""
from skill_seekers.mcp.server import fetch_config_tool
# Mock SourceManager
mock_source_manager = MagicMock()
mock_source_manager.get_source.return_value = {
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"branch": "main",
"token_env": "GITHUB_TOKEN",
}
mock_source_manager_class.return_value = mock_source_manager
# Mock GitConfigRepo
mock_repo_instance = MagicMock()
mock_repo_path = temp_dirs["cache"] / "team"
mock_repo_path.mkdir()
react_config = {"name": "react", "base_url": "https://react.dev/"}
(mock_repo_path / "react.json").write_text(json.dumps(react_config))
mock_repo_instance.clone_or_pull.return_value = mock_repo_path
mock_repo_instance.get_config.return_value = react_config
mock_git_repo_class.return_value = mock_repo_instance
args = {"config_name": "react", "source": "team", "destination": str(temp_dirs["dest"])}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "git source" in result[0].text
assert "team" in result[0].text
# Verify source was retrieved
mock_source_manager.get_source.assert_called_once_with("team")
# Verify file was created
config_file = temp_dirs["dest"] / "react.json"
assert config_file.exists()
async def test_fetch_config_source_not_found(self):
"""Test error when source doesn't exist."""
from skill_seekers.mcp.server import fetch_config_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.get_source.side_effect = KeyError("Source 'nonexistent' not found")
mock_sm_class.return_value = mock_sm
args = {"config_name": "react", "source": "nonexistent"}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "not found" in result[0].text
@patch("skill_seekers.mcp.git_repo.GitConfigRepo")
async def test_fetch_config_config_not_found_in_repo(self, mock_git_repo_class, temp_dirs):
"""Test error when config doesn't exist in repository."""
from skill_seekers.mcp.server import fetch_config_tool
# Mock GitConfigRepo
mock_repo_instance = MagicMock()
mock_repo_path = temp_dirs["cache"] / "temp_django"
mock_repo_path.mkdir()
mock_repo_instance.clone_or_pull.return_value = mock_repo_path
mock_repo_instance.get_config.side_effect = FileNotFoundError(
"Config 'django' not found in repository. Available configs: react, vue"
)
mock_git_repo_class.return_value = mock_repo_instance
args = {"config_name": "django", "git_url": "https://github.com/myorg/configs.git"}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "not found" in result[0].text
assert "Available configs" in result[0].text
@patch("skill_seekers.mcp.git_repo.GitConfigRepo")
async def test_fetch_config_invalid_git_url(self, mock_git_repo_class):
"""Test error handling for invalid git URL."""
from skill_seekers.mcp.server import fetch_config_tool
# Mock GitConfigRepo to raise ValueError
mock_repo_instance = MagicMock()
mock_repo_instance.clone_or_pull.side_effect = ValueError("Invalid git URL: not-a-url")
mock_git_repo_class.return_value = mock_repo_instance
args = {"config_name": "react", "git_url": "not-a-url"}
result = await fetch_config_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "Invalid git URL" in result[0].text
@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP not available")
@pytest.mark.asyncio
class TestSourceManagementTools:
"""Test add/list/remove config source tools."""
async def test_add_config_source(self, temp_dirs):
"""Test adding a new config source."""
from skill_seekers.mcp.server import add_config_source_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.add_source.return_value = {
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"type": "github",
"branch": "main",
"token_env": "GITHUB_TOKEN",
"priority": 100,
"enabled": True,
"added_at": "2025-12-21T10:00:00+00:00",
}
mock_sm_class.return_value = mock_sm
args = {"name": "team", "git_url": "https://github.com/myorg/configs.git"}
result = await add_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "team" in result[0].text
assert "registered" in result[0].text
# Verify add_source was called
mock_sm.add_source.assert_called_once()
async def test_add_config_source_missing_name(self):
"""Test error when name is missing."""
from skill_seekers.mcp.server import add_config_source_tool
args = {"git_url": "https://github.com/myorg/configs.git"}
result = await add_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "name" in result[0].text.lower()
assert "required" in result[0].text.lower()
async def test_add_config_source_missing_git_url(self):
"""Test error when git_url is missing."""
from skill_seekers.mcp.server import add_config_source_tool
args = {"name": "team"}
result = await add_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "git_url" in result[0].text.lower()
assert "required" in result[0].text.lower()
async def test_add_config_source_invalid_name(self):
"""Test error when source name is invalid."""
from skill_seekers.mcp.server import add_config_source_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.add_source.side_effect = ValueError(
"Invalid source name 'team@company'. Must be alphanumeric with optional hyphens/underscores."
)
mock_sm_class.return_value = mock_sm
args = {"name": "team@company", "git_url": "https://github.com/myorg/configs.git"}
result = await add_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "Validation Error" in result[0].text
async def test_list_config_sources(self):
"""Test listing config sources."""
from skill_seekers.mcp.server import list_config_sources_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.list_sources.return_value = [
{
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"type": "github",
"branch": "main",
"token_env": "GITHUB_TOKEN",
"priority": 1,
"enabled": True,
"added_at": "2025-12-21T10:00:00+00:00",
},
{
"name": "company",
"git_url": "https://gitlab.company.com/configs.git",
"type": "gitlab",
"branch": "develop",
"token_env": "GITLAB_TOKEN",
"priority": 2,
"enabled": True,
"added_at": "2025-12-21T11:00:00+00:00",
},
]
mock_sm_class.return_value = mock_sm
args = {}
result = await list_config_sources_tool(args)
assert len(result) == 1
assert "📋" in result[0].text
assert "team" in result[0].text
assert "company" in result[0].text
assert "2 total" in result[0].text
async def test_list_config_sources_empty(self):
"""Test listing when no sources registered."""
from skill_seekers.mcp.server import list_config_sources_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.list_sources.return_value = []
mock_sm_class.return_value = mock_sm
args = {}
result = await list_config_sources_tool(args)
assert len(result) == 1
assert "No config sources registered" in result[0].text
async def test_list_config_sources_enabled_only(self):
"""Test listing only enabled sources."""
from skill_seekers.mcp.server import list_config_sources_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.list_sources.return_value = [
{
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"type": "github",
"branch": "main",
"token_env": "GITHUB_TOKEN",
"priority": 1,
"enabled": True,
"added_at": "2025-12-21T10:00:00+00:00",
}
]
mock_sm_class.return_value = mock_sm
args = {"enabled_only": True}
result = await list_config_sources_tool(args)
assert len(result) == 1
assert "enabled only" in result[0].text
# Verify list_sources was called with correct parameter
mock_sm.list_sources.assert_called_once_with(enabled_only=True)
async def test_remove_config_source(self):
"""Test removing a config source."""
from skill_seekers.mcp.server import remove_config_source_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.remove_source.return_value = True
mock_sm_class.return_value = mock_sm
args = {"name": "team"}
result = await remove_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "removed" in result[0].text.lower()
assert "team" in result[0].text
# Verify remove_source was called
mock_sm.remove_source.assert_called_once_with("team")
async def test_remove_config_source_not_found(self):
"""Test removing non-existent source."""
from skill_seekers.mcp.server import remove_config_source_tool
with patch("skill_seekers.mcp.source_manager.SourceManager") as mock_sm_class:
mock_sm = MagicMock()
mock_sm.remove_source.return_value = False
mock_sm.list_sources.return_value = [
{"name": "team", "git_url": "https://example.com/1.git"},
{"name": "company", "git_url": "https://example.com/2.git"},
]
mock_sm_class.return_value = mock_sm
args = {"name": "nonexistent"}
result = await remove_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "not found" in result[0].text
assert "Available sources" in result[0].text
async def test_remove_config_source_missing_name(self):
"""Test error when name is missing."""
from skill_seekers.mcp.server import remove_config_source_tool
args = {}
result = await remove_config_source_tool(args)
assert len(result) == 1
assert "" in result[0].text
assert "name" in result[0].text.lower()
assert "required" in result[0].text.lower()
@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP not available")
@pytest.mark.asyncio
class TestCompleteWorkflow:
"""Test complete workflow of add → fetch → remove."""
@patch("skill_seekers.mcp.git_repo.GitConfigRepo")
@patch("skill_seekers.mcp.source_manager.SourceManager")
async def test_add_fetch_remove_workflow(self, mock_sm_class, mock_git_repo_class, temp_dirs):
"""Test complete workflow: add source → fetch config → remove source."""
from skill_seekers.mcp.server import (
add_config_source_tool,
fetch_config_tool,
list_config_sources_tool,
remove_config_source_tool,
)
# Step 1: Add source
mock_sm = MagicMock()
mock_sm.add_source.return_value = {
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"type": "github",
"branch": "main",
"token_env": "GITHUB_TOKEN",
"priority": 100,
"enabled": True,
"added_at": "2025-12-21T10:00:00+00:00",
}
mock_sm_class.return_value = mock_sm
add_result = await add_config_source_tool(
{"name": "team", "git_url": "https://github.com/myorg/configs.git"}
)
assert "" in add_result[0].text
# Step 2: Fetch config from source
mock_sm.get_source.return_value = {
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"branch": "main",
"token_env": "GITHUB_TOKEN",
}
mock_repo = MagicMock()
mock_repo_path = temp_dirs["cache"] / "team"
mock_repo_path.mkdir()
react_config = {"name": "react", "base_url": "https://react.dev/"}
(mock_repo_path / "react.json").write_text(json.dumps(react_config))
mock_repo.clone_or_pull.return_value = mock_repo_path
mock_repo.get_config.return_value = react_config
mock_git_repo_class.return_value = mock_repo
fetch_result = await fetch_config_tool(
{"config_name": "react", "source": "team", "destination": str(temp_dirs["dest"])}
)
assert "" in fetch_result[0].text
# Verify config file created
assert (temp_dirs["dest"] / "react.json").exists()
# Step 3: List sources
mock_sm.list_sources.return_value = [
{
"name": "team",
"git_url": "https://github.com/myorg/configs.git",
"type": "github",
"branch": "main",
"token_env": "GITHUB_TOKEN",
"priority": 100,
"enabled": True,
"added_at": "2025-12-21T10:00:00+00:00",
}
]
list_result = await list_config_sources_tool({})
assert "team" in list_result[0].text
# Step 4: Remove source
mock_sm.remove_source.return_value = True
remove_result = await remove_config_source_tool({"name": "team"})
assert "" in remove_result[0].text