Second batch of comprehensive linting fixes: Unused Arguments/Variables (136 errors): - ARG002/ARG001 (91 errors): Prefixed unused method/function arguments with '_' - Interface methods in adaptors (base.py, gemini.py, markdown.py) - AST analyzer methods maintaining signatures (code_analyzer.py) - Test fixtures and hooks (conftest.py) - Added noqa: ARG001/ARG002 for pytest hooks requiring exact names - F841 (45 errors): Prefixed unused local variables with '_' - Tuple unpacking where some values aren't needed - Variables assigned but not referenced Loop & Boolean Quality (28 errors): - B007 (18 errors): Prefixed unused loop control variables with '_' - enumerate() loops where index not used - for-in loops where loop variable not referenced - E712 (10 errors): Simplified boolean comparisons - Changed '== True' to direct boolean check - Changed '== False' to 'not' expression - Improved test readability Code Quality (24 errors): - SIM201 (4 errors): Already fixed in previous commit - SIM118 (2 errors): Already fixed in previous commit - E741 (4 errors): Already fixed in previous commit - Config manager loop variable fix (1 error) All Tests Passing: - test_scraper_features.py: 42 passed - test_integration.py: 51 passed - test_architecture_scenarios.py: 11 passed - test_real_world_fastmcp.py: 19 passed, 1 skipped Note: Some SIM errors (nested if, multiple with) remain unfixed as they would require non-trivial refactoring. Focus was on functional correctness. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
880 lines
30 KiB
Python
880 lines
30 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
E2E Tests for A1.9 Git Source Features
|
|
|
|
Tests the complete workflow with temporary files and repositories:
|
|
1. GitConfigRepo - clone/pull operations
|
|
2. SourceManager - registry CRUD operations
|
|
3. MCP Tools - all 4 git-related tools
|
|
4. Integration - complete user workflows
|
|
5. Error handling - authentication, not found, etc.
|
|
|
|
All tests use temporary directories and actual git repositories.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import git
|
|
import pytest
|
|
|
|
from skill_seekers.mcp.git_repo import GitConfigRepo
|
|
from skill_seekers.mcp.source_manager import SourceManager
|
|
|
|
# Check if MCP is available
|
|
try:
|
|
import mcp # noqa: F401
|
|
from mcp.types import TextContent # noqa: F401
|
|
|
|
MCP_AVAILABLE = True
|
|
except ImportError:
|
|
MCP_AVAILABLE = False
|
|
|
|
|
|
class TestGitSourcesE2E:
|
|
"""End-to-end tests for git source features."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for cache and config."""
|
|
cache_dir = tempfile.mkdtemp(prefix="ss_cache_")
|
|
config_dir = tempfile.mkdtemp(prefix="ss_config_")
|
|
yield cache_dir, config_dir
|
|
# Cleanup
|
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
|
shutil.rmtree(config_dir, ignore_errors=True)
|
|
|
|
@pytest.fixture
|
|
def temp_git_repo(self):
|
|
"""Create a temporary git repository with sample configs."""
|
|
repo_dir = tempfile.mkdtemp(prefix="ss_repo_")
|
|
|
|
# Initialize git repository
|
|
repo = git.Repo.init(repo_dir)
|
|
|
|
# Create sample config files
|
|
configs = {
|
|
"react.json": {
|
|
"name": "react",
|
|
"description": "React framework for UIs",
|
|
"base_url": "https://react.dev/",
|
|
"selectors": {"main_content": "article", "title": "h1", "code_blocks": "pre code"},
|
|
"url_patterns": {"include": [], "exclude": []},
|
|
"categories": {"getting_started": ["learn", "start"], "api": ["reference", "api"]},
|
|
"rate_limit": 0.5,
|
|
"max_pages": 100,
|
|
},
|
|
"vue.json": {
|
|
"name": "vue",
|
|
"description": "Vue.js progressive framework",
|
|
"base_url": "https://vuejs.org/",
|
|
"selectors": {"main_content": "main", "title": "h1"},
|
|
"url_patterns": {"include": [], "exclude": []},
|
|
"categories": {},
|
|
"rate_limit": 0.5,
|
|
"max_pages": 50,
|
|
},
|
|
"django.json": {
|
|
"name": "django",
|
|
"description": "Django web framework",
|
|
"base_url": "https://docs.djangoproject.com/",
|
|
"selectors": {"main_content": "div[role='main']", "title": "h1"},
|
|
"url_patterns": {"include": [], "exclude": []},
|
|
"categories": {},
|
|
"rate_limit": 0.5,
|
|
"max_pages": 200,
|
|
},
|
|
}
|
|
|
|
# Write config files
|
|
for filename, config_data in configs.items():
|
|
config_path = Path(repo_dir) / filename
|
|
with open(config_path, "w") as f:
|
|
json.dump(config_data, f, indent=2)
|
|
|
|
# Add and commit
|
|
repo.index.add(["*.json"])
|
|
repo.index.commit("Initial commit with sample configs")
|
|
|
|
yield repo_dir, repo
|
|
|
|
# Cleanup
|
|
shutil.rmtree(repo_dir, ignore_errors=True)
|
|
|
|
def test_e2e_workflow_direct_git_url(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 1: Direct git URL workflow (no source registration)
|
|
|
|
Steps:
|
|
1. Clone repository via direct git URL
|
|
2. List available configs
|
|
3. Fetch specific config
|
|
4. Verify config content
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Step 1: Clone repository
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name="test-direct",
|
|
git_url=git_url,
|
|
branch="master", # git.Repo.init creates 'master' by default
|
|
)
|
|
|
|
assert repo_path.exists()
|
|
assert (repo_path / ".git").exists()
|
|
|
|
# Step 2: List available configs
|
|
configs = git_repo.find_configs(repo_path)
|
|
assert len(configs) == 3
|
|
config_names = [c.stem for c in configs]
|
|
assert set(config_names) == {"react", "vue", "django"}
|
|
|
|
# Step 3: Fetch specific config
|
|
config = git_repo.get_config(repo_path, "react")
|
|
|
|
# Step 4: Verify config content
|
|
assert config["name"] == "react"
|
|
assert config["description"] == "React framework for UIs"
|
|
assert config["base_url"] == "https://react.dev/"
|
|
assert "selectors" in config
|
|
assert "categories" in config
|
|
assert config["max_pages"] == 100
|
|
|
|
def test_e2e_workflow_with_source_registration(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 2: Complete workflow with source registration
|
|
|
|
Steps:
|
|
1. Add source to registry
|
|
2. List sources
|
|
3. Get source details
|
|
4. Clone via source name
|
|
5. Fetch config
|
|
6. Update source (re-add with different priority)
|
|
7. Remove source
|
|
8. Verify removal
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Step 1: Add source to registry
|
|
source_manager = SourceManager(config_dir=config_dir)
|
|
source = source_manager.add_source(
|
|
name="team-configs", git_url=git_url, source_type="custom", branch="master", priority=10
|
|
)
|
|
|
|
assert source["name"] == "team-configs"
|
|
assert source["git_url"] == git_url
|
|
assert source["type"] == "custom"
|
|
assert source["branch"] == "master"
|
|
assert source["priority"] == 10
|
|
assert source["enabled"] is True
|
|
|
|
# Step 2: List sources
|
|
sources = source_manager.list_sources()
|
|
assert len(sources) == 1
|
|
assert sources[0]["name"] == "team-configs"
|
|
|
|
# Step 3: Get source details
|
|
retrieved_source = source_manager.get_source("team-configs")
|
|
assert retrieved_source["git_url"] == git_url
|
|
|
|
# Step 4: Clone via source name
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name=source["name"], git_url=source["git_url"], branch=source["branch"]
|
|
)
|
|
|
|
assert repo_path.exists()
|
|
|
|
# Step 5: Fetch config
|
|
config = git_repo.get_config(repo_path, "vue")
|
|
assert config["name"] == "vue"
|
|
assert config["base_url"] == "https://vuejs.org/"
|
|
|
|
# Step 6: Update source (re-add with different priority)
|
|
updated_source = source_manager.add_source(
|
|
name="team-configs",
|
|
git_url=git_url,
|
|
source_type="custom",
|
|
branch="master",
|
|
priority=5, # Changed priority
|
|
)
|
|
assert updated_source["priority"] == 5
|
|
|
|
# Step 7: Remove source
|
|
removed = source_manager.remove_source("team-configs")
|
|
assert removed is True
|
|
|
|
# Step 8: Verify removal
|
|
sources = source_manager.list_sources()
|
|
assert len(sources) == 0
|
|
|
|
with pytest.raises(KeyError, match="Source 'team-configs' not found"):
|
|
source_manager.get_source("team-configs")
|
|
|
|
def test_e2e_multiple_sources_priority_resolution(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 3: Multiple sources with priority resolution
|
|
|
|
Steps:
|
|
1. Add multiple sources with different priorities
|
|
2. Verify sources are sorted by priority
|
|
3. Enable/disable sources
|
|
4. List enabled sources only
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
source_manager = SourceManager(config_dir=config_dir)
|
|
|
|
# Step 1: Add multiple sources with different priorities
|
|
source_manager.add_source(name="low-priority", git_url=git_url, priority=100)
|
|
source_manager.add_source(name="high-priority", git_url=git_url, priority=1)
|
|
source_manager.add_source(name="medium-priority", git_url=git_url, priority=50)
|
|
|
|
# Step 2: Verify sources are sorted by priority
|
|
sources = source_manager.list_sources()
|
|
assert len(sources) == 3
|
|
assert sources[0]["name"] == "high-priority"
|
|
assert sources[1]["name"] == "medium-priority"
|
|
assert sources[2]["name"] == "low-priority"
|
|
|
|
# Step 3: Enable/disable sources
|
|
source_manager.add_source(name="high-priority", git_url=git_url, priority=1, enabled=False)
|
|
|
|
# Step 4: List enabled sources only
|
|
enabled_sources = source_manager.list_sources(enabled_only=True)
|
|
assert len(enabled_sources) == 2
|
|
assert all(s["enabled"] for s in enabled_sources)
|
|
assert "high-priority" not in [s["name"] for s in enabled_sources]
|
|
|
|
def test_e2e_pull_existing_repository(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 4: Pull updates from existing repository
|
|
|
|
Steps:
|
|
1. Clone repository
|
|
2. Add new commit to original repo
|
|
3. Pull updates
|
|
4. Verify new config is available
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
|
|
# Step 1: Clone repository
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name="test-pull", git_url=git_url, branch="master"
|
|
)
|
|
|
|
initial_configs = git_repo.find_configs(repo_path)
|
|
assert len(initial_configs) == 3
|
|
|
|
# Step 2: Add new commit to original repo
|
|
new_config = {
|
|
"name": "fastapi",
|
|
"description": "FastAPI framework",
|
|
"base_url": "https://fastapi.tiangolo.com/",
|
|
"selectors": {"main_content": "article"},
|
|
"url_patterns": {"include": [], "exclude": []},
|
|
"categories": {},
|
|
"rate_limit": 0.5,
|
|
"max_pages": 150,
|
|
}
|
|
|
|
new_config_path = Path(repo_dir) / "fastapi.json"
|
|
with open(new_config_path, "w") as f:
|
|
json.dump(new_config, f, indent=2)
|
|
|
|
repo.index.add(["fastapi.json"])
|
|
repo.index.commit("Add FastAPI config")
|
|
|
|
# Step 3: Pull updates
|
|
updated_repo_path = git_repo.clone_or_pull(
|
|
source_name="test-pull",
|
|
git_url=git_url,
|
|
branch="master",
|
|
force_refresh=False, # Should pull, not re-clone
|
|
)
|
|
|
|
# Step 4: Verify new config is available
|
|
updated_configs = git_repo.find_configs(updated_repo_path)
|
|
assert len(updated_configs) == 4
|
|
|
|
fastapi_config = git_repo.get_config(updated_repo_path, "fastapi")
|
|
assert fastapi_config["name"] == "fastapi"
|
|
assert fastapi_config["max_pages"] == 150
|
|
|
|
def test_e2e_force_refresh(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 5: Force refresh (delete and re-clone)
|
|
|
|
Steps:
|
|
1. Clone repository
|
|
2. Modify local cache manually
|
|
3. Force refresh
|
|
4. Verify cache was reset
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
|
|
# Step 1: Clone repository
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name="test-refresh", git_url=git_url, branch="master"
|
|
)
|
|
|
|
# Step 2: Modify local cache manually
|
|
corrupt_file = repo_path / "CORRUPTED.txt"
|
|
with open(corrupt_file, "w") as f:
|
|
f.write("This file should not exist after refresh")
|
|
|
|
assert corrupt_file.exists()
|
|
|
|
# Step 3: Force refresh
|
|
refreshed_repo_path = git_repo.clone_or_pull(
|
|
source_name="test-refresh",
|
|
git_url=git_url,
|
|
branch="master",
|
|
force_refresh=True, # Delete and re-clone
|
|
)
|
|
|
|
# Step 4: Verify cache was reset
|
|
assert not corrupt_file.exists()
|
|
configs = git_repo.find_configs(refreshed_repo_path)
|
|
assert len(configs) == 3
|
|
|
|
def test_e2e_config_not_found(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 6: Error handling - config not found
|
|
|
|
Steps:
|
|
1. Clone repository
|
|
2. Try to fetch non-existent config
|
|
3. Verify helpful error message with suggestions
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
|
|
# Step 1: Clone repository
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name="test-not-found", git_url=git_url, branch="master"
|
|
)
|
|
|
|
# Step 2: Try to fetch non-existent config
|
|
with pytest.raises(FileNotFoundError) as exc_info:
|
|
git_repo.get_config(repo_path, "nonexistent")
|
|
|
|
# Step 3: Verify helpful error message with suggestions
|
|
error_msg = str(exc_info.value)
|
|
assert "nonexistent.json" in error_msg
|
|
assert "not found" in error_msg
|
|
assert "react" in error_msg # Should suggest available configs
|
|
assert "vue" in error_msg
|
|
assert "django" in error_msg
|
|
|
|
def test_e2e_invalid_git_url(self, temp_dirs):
|
|
"""
|
|
E2E Test 7: Error handling - invalid git URL
|
|
|
|
Steps:
|
|
1. Try to clone with invalid URL
|
|
2. Verify validation error
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
|
|
# Invalid URLs
|
|
invalid_urls = ["", "not-a-url", "ftp://invalid.com/repo.git", "javascript:alert('xss')"]
|
|
|
|
for invalid_url in invalid_urls:
|
|
with pytest.raises(ValueError, match="Invalid git URL"):
|
|
git_repo.clone_or_pull(
|
|
source_name="test-invalid", git_url=invalid_url, branch="master"
|
|
)
|
|
|
|
def test_e2e_source_name_validation(self, temp_dirs):
|
|
"""
|
|
E2E Test 8: Error handling - invalid source names
|
|
|
|
Steps:
|
|
1. Try to add sources with invalid names
|
|
2. Verify validation errors
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
source_manager = SourceManager(config_dir=config_dir)
|
|
|
|
# Invalid source names
|
|
invalid_names = [
|
|
"",
|
|
"name with spaces",
|
|
"name/with/slashes",
|
|
"name@with@symbols",
|
|
"name.with.dots",
|
|
"123-only-numbers-start-is-ok", # This should actually work
|
|
"name!exclamation",
|
|
]
|
|
|
|
valid_git_url = "https://github.com/test/repo.git"
|
|
|
|
for invalid_name in invalid_names[:-2]: # Skip the valid one
|
|
if invalid_name == "123-only-numbers-start-is-ok":
|
|
continue
|
|
with pytest.raises(ValueError, match="Invalid source name"):
|
|
source_manager.add_source(name=invalid_name, git_url=valid_git_url)
|
|
|
|
def test_e2e_registry_persistence(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 9: Registry persistence across instances
|
|
|
|
Steps:
|
|
1. Add source with one SourceManager instance
|
|
2. Create new SourceManager instance
|
|
3. Verify source persists
|
|
4. Modify source with new instance
|
|
5. Verify changes persist
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Step 1: Add source with one instance
|
|
manager1 = SourceManager(config_dir=config_dir)
|
|
manager1.add_source(name="persistent-source", git_url=git_url, priority=25)
|
|
|
|
# Step 2: Create new instance
|
|
manager2 = SourceManager(config_dir=config_dir)
|
|
|
|
# Step 3: Verify source persists
|
|
sources = manager2.list_sources()
|
|
assert len(sources) == 1
|
|
assert sources[0]["name"] == "persistent-source"
|
|
assert sources[0]["priority"] == 25
|
|
|
|
# Step 4: Modify source with new instance
|
|
manager2.add_source(
|
|
name="persistent-source",
|
|
git_url=git_url,
|
|
priority=50, # Changed
|
|
)
|
|
|
|
# Step 5: Verify changes persist
|
|
manager3 = SourceManager(config_dir=config_dir)
|
|
source = manager3.get_source("persistent-source")
|
|
assert source["priority"] == 50
|
|
|
|
def test_e2e_cache_isolation(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 10: Cache isolation between different cache directories
|
|
|
|
Steps:
|
|
1. Clone to cache_dir_1
|
|
2. Clone same repo to cache_dir_2
|
|
3. Verify both caches are independent
|
|
4. Modify one cache
|
|
5. Verify other cache is unaffected
|
|
"""
|
|
_config_dir = temp_dirs[1]
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
cache_dir_1 = tempfile.mkdtemp(prefix="ss_cache1_")
|
|
cache_dir_2 = tempfile.mkdtemp(prefix="ss_cache2_")
|
|
|
|
try:
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Step 1: Clone to cache_dir_1
|
|
git_repo_1 = GitConfigRepo(cache_dir=cache_dir_1)
|
|
repo_path_1 = git_repo_1.clone_or_pull(
|
|
source_name="test-source", git_url=git_url, branch="master"
|
|
)
|
|
|
|
# Step 2: Clone same repo to cache_dir_2
|
|
git_repo_2 = GitConfigRepo(cache_dir=cache_dir_2)
|
|
repo_path_2 = git_repo_2.clone_or_pull(
|
|
source_name="test-source", git_url=git_url, branch="master"
|
|
)
|
|
|
|
# Step 3: Verify both caches are independent
|
|
assert repo_path_1 != repo_path_2
|
|
assert repo_path_1.exists()
|
|
assert repo_path_2.exists()
|
|
|
|
# Step 4: Modify one cache
|
|
marker_file = repo_path_1 / "MARKER.txt"
|
|
with open(marker_file, "w") as f:
|
|
f.write("Cache 1 marker")
|
|
|
|
# Step 5: Verify other cache is unaffected
|
|
assert marker_file.exists()
|
|
assert not (repo_path_2 / "MARKER.txt").exists()
|
|
|
|
configs_1 = git_repo_1.find_configs(repo_path_1)
|
|
configs_2 = git_repo_2.find_configs(repo_path_2)
|
|
assert len(configs_1) == len(configs_2) == 3
|
|
|
|
finally:
|
|
shutil.rmtree(cache_dir_1, ignore_errors=True)
|
|
shutil.rmtree(cache_dir_2, ignore_errors=True)
|
|
|
|
def test_e2e_auto_detect_token_env(self, temp_dirs):
|
|
"""
|
|
E2E Test 11: Auto-detect token_env based on source type
|
|
|
|
Steps:
|
|
1. Add GitHub source without token_env
|
|
2. Verify GITHUB_TOKEN was auto-detected
|
|
3. Add GitLab source without token_env
|
|
4. Verify GITLAB_TOKEN was auto-detected
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
source_manager = SourceManager(config_dir=config_dir)
|
|
|
|
# Step 1: Add GitHub source
|
|
github_source = source_manager.add_source(
|
|
name="github-test",
|
|
git_url="https://github.com/test/repo.git",
|
|
source_type="github",
|
|
# No token_env specified
|
|
)
|
|
|
|
# Step 2: Verify GITHUB_TOKEN was auto-detected
|
|
assert github_source["token_env"] == "GITHUB_TOKEN"
|
|
|
|
# Step 3: Add GitLab source
|
|
gitlab_source = source_manager.add_source(
|
|
name="gitlab-test",
|
|
git_url="https://gitlab.com/test/repo.git",
|
|
source_type="gitlab",
|
|
# No token_env specified
|
|
)
|
|
|
|
# Step 4: Verify GITLAB_TOKEN was auto-detected
|
|
assert gitlab_source["token_env"] == "GITLAB_TOKEN"
|
|
|
|
# Also test custom type (defaults to GIT_TOKEN)
|
|
custom_source = source_manager.add_source(
|
|
name="custom-test", git_url="https://custom.com/test/repo.git", source_type="custom"
|
|
)
|
|
assert custom_source["token_env"] == "GIT_TOKEN"
|
|
|
|
def test_e2e_complete_user_workflow(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
E2E Test 12: Complete real-world user workflow
|
|
|
|
Simulates a team using the feature end-to-end:
|
|
1. Team lead creates config repository
|
|
2. Team lead registers source
|
|
3. Developer 1 clones and uses config
|
|
4. Developer 2 uses same source (cached)
|
|
5. Team lead updates repository
|
|
6. Developers pull updates
|
|
7. Config is removed from repo
|
|
8. Error handling works correctly
|
|
"""
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Step 1: Team lead creates repository (already done by fixture)
|
|
|
|
# Step 2: Team lead registers source
|
|
source_manager = SourceManager(config_dir=config_dir)
|
|
source_manager.add_source(
|
|
name="team-configs", git_url=git_url, source_type="custom", branch="master", priority=1
|
|
)
|
|
|
|
# Step 3: Developer 1 clones and uses config
|
|
git_repo = GitConfigRepo(cache_dir=cache_dir)
|
|
source = source_manager.get_source("team-configs")
|
|
repo_path = git_repo.clone_or_pull(
|
|
source_name=source["name"], git_url=source["git_url"], branch=source["branch"]
|
|
)
|
|
|
|
react_config = git_repo.get_config(repo_path, "react")
|
|
assert react_config["name"] == "react"
|
|
|
|
# Step 4: Developer 2 uses same source (should use cache, not re-clone)
|
|
# Simulate by checking if pull works (not re-clone)
|
|
repo_path_2 = git_repo.clone_or_pull(
|
|
source_name=source["name"], git_url=source["git_url"], branch=source["branch"]
|
|
)
|
|
assert repo_path == repo_path_2
|
|
|
|
# Step 5: Team lead updates repository
|
|
updated_react_config = react_config.copy()
|
|
updated_react_config["max_pages"] = 500 # Increased limit
|
|
|
|
react_config_path = Path(repo_dir) / "react.json"
|
|
with open(react_config_path, "w") as f:
|
|
json.dump(updated_react_config, f, indent=2)
|
|
|
|
repo.index.add(["react.json"])
|
|
repo.index.commit("Increase React config max_pages to 500")
|
|
|
|
# Step 6: Developers pull updates
|
|
git_repo.clone_or_pull(
|
|
source_name=source["name"], git_url=source["git_url"], branch=source["branch"]
|
|
)
|
|
|
|
updated_config = git_repo.get_config(repo_path, "react")
|
|
assert updated_config["max_pages"] == 500
|
|
|
|
# Step 7: Config is removed from repo
|
|
react_config_path.unlink()
|
|
repo.index.remove(["react.json"])
|
|
repo.index.commit("Remove react.json")
|
|
|
|
git_repo.clone_or_pull(
|
|
source_name=source["name"], git_url=source["git_url"], branch=source["branch"]
|
|
)
|
|
|
|
# Step 8: Error handling works correctly
|
|
with pytest.raises(FileNotFoundError, match="react.json"):
|
|
git_repo.get_config(repo_path, "react")
|
|
|
|
# But other configs still work
|
|
vue_config = git_repo.get_config(repo_path, "vue")
|
|
assert vue_config["name"] == "vue"
|
|
|
|
|
|
@pytest.mark.skipif(not MCP_AVAILABLE, reason="MCP not installed")
|
|
class TestMCPToolsE2E:
|
|
"""E2E tests for MCP tools integration."""
|
|
|
|
@pytest.fixture
|
|
def temp_dirs(self):
|
|
"""Create temporary directories for cache and config."""
|
|
cache_dir = tempfile.mkdtemp(prefix="ss_mcp_cache_")
|
|
config_dir = tempfile.mkdtemp(prefix="ss_mcp_config_")
|
|
|
|
# Set environment variables for tools to use
|
|
os.environ["SKILL_SEEKERS_CACHE_DIR"] = cache_dir
|
|
os.environ["SKILL_SEEKERS_CONFIG_DIR"] = config_dir
|
|
|
|
yield cache_dir, config_dir
|
|
|
|
# Cleanup
|
|
os.environ.pop("SKILL_SEEKERS_CACHE_DIR", None)
|
|
os.environ.pop("SKILL_SEEKERS_CONFIG_DIR", None)
|
|
shutil.rmtree(cache_dir, ignore_errors=True)
|
|
shutil.rmtree(config_dir, ignore_errors=True)
|
|
|
|
@pytest.fixture
|
|
def temp_git_repo(self):
|
|
"""Create a temporary git repository with sample configs."""
|
|
repo_dir = tempfile.mkdtemp(prefix="ss_mcp_repo_")
|
|
|
|
# Initialize git repository
|
|
repo = git.Repo.init(repo_dir)
|
|
|
|
# Create sample config
|
|
config = {
|
|
"name": "test-framework",
|
|
"description": "Test framework for E2E",
|
|
"base_url": "https://example.com/docs/",
|
|
"selectors": {"main_content": "article", "title": "h1"},
|
|
"url_patterns": {"include": [], "exclude": []},
|
|
"categories": {},
|
|
"rate_limit": 0.5,
|
|
"max_pages": 50,
|
|
}
|
|
|
|
config_path = Path(repo_dir) / "test-framework.json"
|
|
with open(config_path, "w") as f:
|
|
json.dump(config, f, indent=2)
|
|
|
|
repo.index.add(["*.json"])
|
|
repo.index.commit("Initial commit")
|
|
|
|
yield repo_dir, repo
|
|
|
|
shutil.rmtree(repo_dir, ignore_errors=True)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_add_list_remove_source_e2e(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
MCP E2E Test 1: Complete add/list/remove workflow via MCP tools
|
|
"""
|
|
from skill_seekers.mcp.server import (
|
|
add_config_source_tool,
|
|
list_config_sources_tool,
|
|
remove_config_source_tool,
|
|
)
|
|
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Add source
|
|
add_result = await add_config_source_tool(
|
|
{
|
|
"name": "mcp-test-source",
|
|
"git_url": git_url,
|
|
"source_type": "custom",
|
|
"branch": "master",
|
|
}
|
|
)
|
|
|
|
assert len(add_result) == 1
|
|
assert "✅" in add_result[0].text
|
|
assert "mcp-test-source" in add_result[0].text
|
|
|
|
# List sources
|
|
list_result = await list_config_sources_tool({})
|
|
|
|
assert len(list_result) == 1
|
|
assert "mcp-test-source" in list_result[0].text
|
|
|
|
# Remove source
|
|
remove_result = await remove_config_source_tool({"name": "mcp-test-source"})
|
|
|
|
assert len(remove_result) == 1
|
|
assert "✅" in remove_result[0].text
|
|
assert "removed" in remove_result[0].text.lower()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_fetch_config_git_url_mode_e2e(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
MCP E2E Test 2: fetch_config with direct git URL
|
|
"""
|
|
from skill_seekers.mcp.server import fetch_config_tool
|
|
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Create destination directory
|
|
dest_dir = Path(config_dir) / "configs"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
result = await fetch_config_tool(
|
|
{
|
|
"config_name": "test-framework",
|
|
"git_url": git_url,
|
|
"branch": "master",
|
|
"destination": str(dest_dir),
|
|
}
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert "✅" in result[0].text
|
|
assert "test-framework" in result[0].text
|
|
|
|
# Verify config was saved
|
|
saved_config = dest_dir / "test-framework.json"
|
|
assert saved_config.exists()
|
|
|
|
with open(saved_config) as f:
|
|
config_data = json.load(f)
|
|
|
|
assert config_data["name"] == "test-framework"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_fetch_config_source_mode_e2e(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
MCP E2E Test 3: fetch_config with registered source
|
|
"""
|
|
from skill_seekers.mcp.server import add_config_source_tool, fetch_config_tool
|
|
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Register source first
|
|
await add_config_source_tool(
|
|
{"name": "test-source", "git_url": git_url, "source_type": "custom", "branch": "master"}
|
|
)
|
|
|
|
# Fetch via source name
|
|
dest_dir = Path(config_dir) / "configs"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
result = await fetch_config_tool(
|
|
{"config_name": "test-framework", "source": "test-source", "destination": str(dest_dir)}
|
|
)
|
|
|
|
assert len(result) == 1
|
|
assert "✅" in result[0].text
|
|
assert "test-framework" in result[0].text
|
|
|
|
# Verify config was saved
|
|
saved_config = dest_dir / "test-framework.json"
|
|
assert saved_config.exists()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_mcp_error_handling_e2e(self, temp_dirs, temp_git_repo):
|
|
"""
|
|
MCP E2E Test 4: Error handling across all tools
|
|
"""
|
|
from skill_seekers.mcp.server import (
|
|
add_config_source_tool,
|
|
fetch_config_tool,
|
|
remove_config_source_tool,
|
|
)
|
|
|
|
cache_dir, config_dir = temp_dirs
|
|
repo_dir, repo = temp_git_repo
|
|
git_url = f"file://{repo_dir}"
|
|
|
|
# Test 1: Add source without name
|
|
result = await add_config_source_tool({"git_url": git_url})
|
|
assert "❌" in result[0].text
|
|
assert "name" in result[0].text.lower()
|
|
|
|
# Test 2: Add source without git_url
|
|
result = await add_config_source_tool({"name": "test"})
|
|
assert "❌" in result[0].text
|
|
assert "git_url" in result[0].text.lower()
|
|
|
|
# Test 3: Remove non-existent source
|
|
result = await remove_config_source_tool({"name": "non-existent"})
|
|
assert "❌" in result[0].text or "not found" in result[0].text.lower()
|
|
|
|
# Test 4: Fetch config from non-existent source
|
|
dest_dir = Path(config_dir) / "configs"
|
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
result = await fetch_config_tool(
|
|
{"config_name": "test", "source": "non-existent-source", "destination": str(dest_dir)}
|
|
)
|
|
assert "❌" in result[0].text or "not found" in result[0].text.lower()
|
|
|
|
# Test 5: Fetch non-existent config from valid source
|
|
await add_config_source_tool(
|
|
{"name": "valid-source", "git_url": git_url, "branch": "master"}
|
|
)
|
|
|
|
result = await fetch_config_tool(
|
|
{
|
|
"config_name": "non-existent-config",
|
|
"source": "valid-source",
|
|
"destination": str(dest_dir),
|
|
}
|
|
)
|
|
assert "❌" in result[0].text or "not found" in result[0].text.lower()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v", "--tb=short"])
|