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>
413 lines
15 KiB
Python
413 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for GitConfigRepo class (git repository operations)
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from git.exc import GitCommandError
|
|
|
|
from skill_seekers.mcp.git_repo import GitConfigRepo
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_cache_dir(tmp_path):
|
|
"""Create temporary cache directory for tests."""
|
|
cache_dir = tmp_path / "test_cache"
|
|
cache_dir.mkdir()
|
|
return cache_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def git_repo(temp_cache_dir):
|
|
"""Create GitConfigRepo instance with temp cache."""
|
|
return GitConfigRepo(cache_dir=str(temp_cache_dir))
|
|
|
|
|
|
class TestGitConfigRepoInit:
|
|
"""Test GitConfigRepo initialization."""
|
|
|
|
def test_init_with_custom_cache_dir(self, temp_cache_dir):
|
|
"""Test initialization with custom cache directory."""
|
|
repo = GitConfigRepo(cache_dir=str(temp_cache_dir))
|
|
assert repo.cache_dir == temp_cache_dir
|
|
assert temp_cache_dir.exists()
|
|
|
|
def test_init_with_env_var(self, tmp_path, monkeypatch):
|
|
"""Test initialization with environment variable."""
|
|
env_cache = tmp_path / "env_cache"
|
|
monkeypatch.setenv("SKILL_SEEKERS_CACHE_DIR", str(env_cache))
|
|
|
|
repo = GitConfigRepo()
|
|
assert repo.cache_dir == env_cache
|
|
assert env_cache.exists()
|
|
|
|
def test_init_with_default(self, monkeypatch):
|
|
"""Test initialization with default cache directory."""
|
|
monkeypatch.delenv("SKILL_SEEKERS_CACHE_DIR", raising=False)
|
|
|
|
repo = GitConfigRepo()
|
|
expected = Path.home() / ".skill-seekers" / "cache"
|
|
assert repo.cache_dir == expected
|
|
|
|
|
|
class TestValidateGitUrl:
|
|
"""Test git URL validation."""
|
|
|
|
def test_validate_https_url(self):
|
|
"""Test validation of HTTPS URLs."""
|
|
assert GitConfigRepo.validate_git_url("https://github.com/org/repo.git")
|
|
assert GitConfigRepo.validate_git_url("https://gitlab.com/org/repo.git")
|
|
|
|
def test_validate_http_url(self):
|
|
"""Test validation of HTTP URLs."""
|
|
assert GitConfigRepo.validate_git_url("http://example.com/repo.git")
|
|
|
|
def test_validate_ssh_url(self):
|
|
"""Test validation of SSH URLs."""
|
|
assert GitConfigRepo.validate_git_url("git@github.com:org/repo.git")
|
|
assert GitConfigRepo.validate_git_url("git@gitlab.com:group/project.git")
|
|
|
|
def test_validate_file_url(self):
|
|
"""Test validation of file:// URLs."""
|
|
assert GitConfigRepo.validate_git_url("file:///path/to/repo.git")
|
|
|
|
def test_invalid_empty_url(self):
|
|
"""Test validation rejects empty URLs."""
|
|
assert not GitConfigRepo.validate_git_url("")
|
|
assert not GitConfigRepo.validate_git_url(None)
|
|
|
|
def test_invalid_malformed_url(self):
|
|
"""Test validation rejects malformed URLs."""
|
|
assert not GitConfigRepo.validate_git_url("not-a-url")
|
|
assert not GitConfigRepo.validate_git_url("ftp://example.com/repo")
|
|
|
|
def test_invalid_ssh_without_colon(self):
|
|
"""Test validation rejects SSH URLs without colon."""
|
|
assert not GitConfigRepo.validate_git_url("git@github.com/org/repo.git")
|
|
|
|
|
|
class TestInjectToken:
|
|
"""Test token injection into git URLs."""
|
|
|
|
def test_inject_token_https(self):
|
|
"""Test token injection into HTTPS URL."""
|
|
url = "https://github.com/org/repo.git"
|
|
token = "ghp_testtoken123"
|
|
|
|
result = GitConfigRepo.inject_token(url, token)
|
|
assert result == "https://ghp_testtoken123@github.com/org/repo.git"
|
|
|
|
def test_inject_token_ssh_to_https(self):
|
|
"""Test SSH URL conversion to HTTPS with token."""
|
|
url = "git@github.com:org/repo.git"
|
|
token = "ghp_testtoken123"
|
|
|
|
result = GitConfigRepo.inject_token(url, token)
|
|
assert result == "https://ghp_testtoken123@github.com/org/repo.git"
|
|
|
|
def test_inject_token_with_port(self):
|
|
"""Test token injection with custom port."""
|
|
url = "https://gitlab.example.com:8443/org/repo.git"
|
|
token = "token123"
|
|
|
|
result = GitConfigRepo.inject_token(url, token)
|
|
assert result == "https://token123@gitlab.example.com:8443/org/repo.git"
|
|
|
|
def test_inject_token_gitlab_ssh(self):
|
|
"""Test GitLab SSH URL conversion."""
|
|
url = "git@gitlab.com:group/project.git"
|
|
token = "glpat-token123"
|
|
|
|
result = GitConfigRepo.inject_token(url, token)
|
|
assert result == "https://glpat-token123@gitlab.com/group/project.git"
|
|
|
|
|
|
class TestCloneOrPull:
|
|
"""Test clone and pull operations."""
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo.clone_from")
|
|
def test_clone_new_repo(self, mock_clone, git_repo):
|
|
"""Test cloning a new repository."""
|
|
mock_clone.return_value = MagicMock()
|
|
|
|
result = git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/repo.git"
|
|
)
|
|
|
|
assert result == git_repo.cache_dir / "test-source"
|
|
mock_clone.assert_called_once()
|
|
|
|
# Verify shallow clone parameters
|
|
call_kwargs = mock_clone.call_args[1]
|
|
assert call_kwargs["depth"] == 1
|
|
assert call_kwargs["single_branch"] is True
|
|
assert call_kwargs["branch"] == "main"
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo")
|
|
def test_pull_existing_repo(self, mock_repo_class, git_repo, temp_cache_dir):
|
|
"""Test pulling updates to existing repository."""
|
|
# Create fake existing repo
|
|
repo_path = temp_cache_dir / "test-source"
|
|
repo_path.mkdir()
|
|
(repo_path / ".git").mkdir()
|
|
|
|
# Mock git.Repo
|
|
mock_repo = MagicMock()
|
|
mock_origin = MagicMock()
|
|
mock_repo.remotes.origin = mock_origin
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
result = git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/repo.git"
|
|
)
|
|
|
|
assert result == repo_path
|
|
mock_origin.pull.assert_called_once_with("main")
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo")
|
|
def test_pull_with_token_update(self, mock_repo_class, git_repo, temp_cache_dir):
|
|
"""Test pulling with token updates remote URL."""
|
|
# Create fake existing repo
|
|
repo_path = temp_cache_dir / "test-source"
|
|
repo_path.mkdir()
|
|
(repo_path / ".git").mkdir()
|
|
|
|
# Mock git.Repo
|
|
mock_repo = MagicMock()
|
|
mock_origin = MagicMock()
|
|
mock_repo.remotes.origin = mock_origin
|
|
mock_repo_class.return_value = mock_repo
|
|
|
|
_result = git_repo.clone_or_pull(
|
|
source_name="test-source",
|
|
git_url="https://github.com/org/repo.git",
|
|
token="ghp_token123",
|
|
)
|
|
|
|
# Verify URL was updated with token
|
|
mock_origin.set_url.assert_called_once()
|
|
updated_url = mock_origin.set_url.call_args[0][0]
|
|
assert "ghp_token123@github.com" in updated_url
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo.clone_from")
|
|
def test_force_refresh_deletes_cache(self, mock_clone, git_repo, temp_cache_dir):
|
|
"""Test force refresh deletes existing cache."""
|
|
# Create fake existing repo
|
|
repo_path = temp_cache_dir / "test-source"
|
|
repo_path.mkdir()
|
|
(repo_path / ".git").mkdir()
|
|
(repo_path / "config.json").write_text("{}")
|
|
|
|
mock_clone.return_value = MagicMock()
|
|
|
|
git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/repo.git", force_refresh=True
|
|
)
|
|
|
|
# Verify clone was called (not pull)
|
|
mock_clone.assert_called_once()
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo.clone_from")
|
|
def test_clone_with_custom_branch(self, mock_clone, git_repo):
|
|
"""Test cloning with custom branch."""
|
|
mock_clone.return_value = MagicMock()
|
|
|
|
git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/repo.git", branch="develop"
|
|
)
|
|
|
|
call_kwargs = mock_clone.call_args[1]
|
|
assert call_kwargs["branch"] == "develop"
|
|
|
|
def test_clone_invalid_url_raises_error(self, git_repo):
|
|
"""Test cloning with invalid URL raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid git URL"):
|
|
git_repo.clone_or_pull(source_name="test-source", git_url="not-a-valid-url")
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo.clone_from")
|
|
def test_clone_auth_failure_error(self, mock_clone, git_repo):
|
|
"""Test authentication failure error handling."""
|
|
mock_clone.side_effect = GitCommandError(
|
|
"clone", 128, stderr="fatal: Authentication failed"
|
|
)
|
|
|
|
with pytest.raises(GitCommandError, match="Authentication failed"):
|
|
git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/repo.git"
|
|
)
|
|
|
|
@patch("skill_seekers.mcp.git_repo.git.Repo.clone_from")
|
|
def test_clone_not_found_error(self, mock_clone, git_repo):
|
|
"""Test repository not found error handling."""
|
|
mock_clone.side_effect = GitCommandError("clone", 128, stderr="fatal: repository not found")
|
|
|
|
with pytest.raises(GitCommandError, match="Repository not found"):
|
|
git_repo.clone_or_pull(
|
|
source_name="test-source", git_url="https://github.com/org/nonexistent.git"
|
|
)
|
|
|
|
|
|
class TestFindConfigs:
|
|
"""Test config file discovery."""
|
|
|
|
def test_find_configs_in_root(self, git_repo, temp_cache_dir):
|
|
"""Test finding config files in repository root."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
(repo_path / "config1.json").write_text("{}")
|
|
(repo_path / "config2.json").write_text("{}")
|
|
(repo_path / "README.md").write_text("# Readme")
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert len(configs) == 2
|
|
assert all(c.suffix == ".json" for c in configs)
|
|
assert sorted([c.name for c in configs]) == ["config1.json", "config2.json"]
|
|
|
|
def test_find_configs_in_subdirs(self, git_repo, temp_cache_dir):
|
|
"""Test finding config files in subdirectories."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
configs_dir = repo_path / "configs"
|
|
configs_dir.mkdir(parents=True)
|
|
|
|
(repo_path / "root.json").write_text("{}")
|
|
(configs_dir / "sub1.json").write_text("{}")
|
|
(configs_dir / "sub2.json").write_text("{}")
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert len(configs) == 3
|
|
|
|
def test_find_configs_excludes_git_dir(self, git_repo, temp_cache_dir):
|
|
"""Test that .git directory is excluded from config search."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
git_dir = repo_path / ".git" / "config"
|
|
git_dir.mkdir(parents=True)
|
|
|
|
(repo_path / "config.json").write_text("{}")
|
|
(git_dir / "internal.json").write_text("{}")
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert len(configs) == 1
|
|
assert configs[0].name == "config.json"
|
|
|
|
def test_find_configs_empty_repo(self, git_repo, temp_cache_dir):
|
|
"""Test finding configs in empty repository."""
|
|
repo_path = temp_cache_dir / "empty-repo"
|
|
repo_path.mkdir()
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert configs == []
|
|
|
|
def test_find_configs_nonexistent_repo(self, git_repo, temp_cache_dir):
|
|
"""Test finding configs in non-existent repository."""
|
|
repo_path = temp_cache_dir / "nonexistent"
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert configs == []
|
|
|
|
def test_find_configs_sorted_by_name(self, git_repo, temp_cache_dir):
|
|
"""Test that configs are sorted by filename."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
(repo_path / "zebra.json").write_text("{}")
|
|
(repo_path / "alpha.json").write_text("{}")
|
|
(repo_path / "beta.json").write_text("{}")
|
|
|
|
configs = git_repo.find_configs(repo_path)
|
|
|
|
assert [c.name for c in configs] == ["alpha.json", "beta.json", "zebra.json"]
|
|
|
|
|
|
class TestGetConfig:
|
|
"""Test config file loading."""
|
|
|
|
def test_get_config_exact_match(self, git_repo, temp_cache_dir):
|
|
"""Test loading config with exact filename match."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
config_data = {"name": "react", "version": "1.0"}
|
|
(repo_path / "react.json").write_text(json.dumps(config_data))
|
|
|
|
result = git_repo.get_config(repo_path, "react")
|
|
|
|
assert result == config_data
|
|
|
|
def test_get_config_with_json_extension(self, git_repo, temp_cache_dir):
|
|
"""Test loading config when .json extension is provided."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
config_data = {"name": "vue"}
|
|
(repo_path / "vue.json").write_text(json.dumps(config_data))
|
|
|
|
result = git_repo.get_config(repo_path, "vue.json")
|
|
|
|
assert result == config_data
|
|
|
|
def test_get_config_case_insensitive(self, git_repo, temp_cache_dir):
|
|
"""Test loading config with case-insensitive match."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
config_data = {"name": "Django"}
|
|
(repo_path / "Django.json").write_text(json.dumps(config_data))
|
|
|
|
result = git_repo.get_config(repo_path, "django")
|
|
|
|
assert result == config_data
|
|
|
|
def test_get_config_in_subdir(self, git_repo, temp_cache_dir):
|
|
"""Test loading config from subdirectory."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
configs_dir = repo_path / "configs"
|
|
configs_dir.mkdir(parents=True)
|
|
|
|
config_data = {"name": "nestjs"}
|
|
(configs_dir / "nestjs.json").write_text(json.dumps(config_data))
|
|
|
|
result = git_repo.get_config(repo_path, "nestjs")
|
|
|
|
assert result == config_data
|
|
|
|
def test_get_config_not_found(self, git_repo, temp_cache_dir):
|
|
"""Test error when config not found."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
(repo_path / "react.json").write_text("{}")
|
|
|
|
with pytest.raises(FileNotFoundError, match="Config 'vue.json' not found"):
|
|
git_repo.get_config(repo_path, "vue")
|
|
|
|
def test_get_config_not_found_shows_available(self, git_repo, temp_cache_dir):
|
|
"""Test error message shows available configs."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
(repo_path / "react.json").write_text("{}")
|
|
(repo_path / "vue.json").write_text("{}")
|
|
|
|
with pytest.raises(FileNotFoundError, match="Available configs: react, vue"):
|
|
git_repo.get_config(repo_path, "django")
|
|
|
|
def test_get_config_invalid_json(self, git_repo, temp_cache_dir):
|
|
"""Test error handling for invalid JSON."""
|
|
repo_path = temp_cache_dir / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
(repo_path / "broken.json").write_text("{ invalid json }")
|
|
|
|
with pytest.raises(ValueError, match="Invalid JSON"):
|
|
git_repo.get_config(repo_path, "broken")
|