Files
syndarix/mcp-servers/git-ops/tests/test_git_wrapper.py
Felipe Cardoso 011b21bf0a refactor(tests): adjust formatting for consistency and readability
- Updated line breaks and indentation across test modules to enhance clarity and maintain consistent style.
- Applied changes to workspace, provider, server, and GitWrapper-related test cases. No functional changes introduced.
2026-01-07 09:17:26 +01:00

944 lines
33 KiB
Python

"""
Tests for the git_wrapper module.
"""
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from git import GitCommandError
from exceptions import (
BranchExistsError,
BranchNotFoundError,
CheckoutError,
CloneError,
CommitError,
GitError,
PullError,
PushError,
)
from git_wrapper import GitWrapper, run_in_executor
from models import FileChangeType
class TestGitWrapperInit:
"""Tests for GitWrapper initialization."""
def test_init_with_valid_path(self, temp_workspace, test_settings):
"""Test initialization with a valid path."""
wrapper = GitWrapper(temp_workspace, test_settings)
assert wrapper.workspace_path == temp_workspace
assert wrapper.settings == test_settings
def test_repo_property_raises_on_non_git(self, temp_workspace, test_settings):
"""Test that accessing repo on non-git dir raises error."""
wrapper = GitWrapper(temp_workspace, test_settings)
with pytest.raises(GitError, match="Not a git repository"):
_ = wrapper.repo
def test_repo_property_works_on_git_dir(self, git_repo, test_settings):
"""Test that repo property works for git directory."""
wrapper = GitWrapper(Path(git_repo.working_dir), test_settings)
assert wrapper.repo is not None
assert wrapper.repo.head is not None
class TestGitWrapperStatus:
"""Tests for git status operations."""
@pytest.mark.asyncio
async def test_status_clean_repo(self, git_wrapper_with_repo):
"""Test status on a clean repository."""
result = await git_wrapper_with_repo.status()
assert result.branch == "main"
assert result.is_clean is True
assert len(result.staged) == 0
assert len(result.unstaged) == 0
assert len(result.untracked) == 0
@pytest.mark.asyncio
async def test_status_with_untracked(self, git_wrapper_with_repo, git_repo):
"""Test status with untracked files."""
# Create untracked file
untracked_file = Path(git_repo.working_dir) / "untracked.txt"
untracked_file.write_text("untracked content")
result = await git_wrapper_with_repo.status()
assert result.is_clean is False
assert "untracked.txt" in result.untracked
@pytest.mark.asyncio
async def test_status_with_modified(self, git_wrapper_with_repo, git_repo):
"""Test status with modified files."""
# Modify existing file
readme = Path(git_repo.working_dir) / "README.md"
readme.write_text("# Modified content\n")
result = await git_wrapper_with_repo.status()
assert result.is_clean is False
assert len(result.unstaged) > 0
@pytest.mark.asyncio
async def test_status_with_staged(self, git_wrapper_with_repo, git_repo):
"""Test status with staged changes."""
# Create and stage a file
new_file = Path(git_repo.working_dir) / "staged.txt"
new_file.write_text("staged content")
git_repo.index.add(["staged.txt"])
result = await git_wrapper_with_repo.status()
assert result.is_clean is False
assert len(result.staged) > 0
@pytest.mark.asyncio
async def test_status_exclude_untracked(self, git_wrapper_with_repo, git_repo):
"""Test status without untracked files."""
untracked_file = Path(git_repo.working_dir) / "untracked.txt"
untracked_file.write_text("untracked")
result = await git_wrapper_with_repo.status(include_untracked=False)
assert len(result.untracked) == 0
class TestGitWrapperBranch:
"""Tests for branch operations."""
@pytest.mark.asyncio
async def test_create_branch(self, git_wrapper_with_repo):
"""Test creating a new branch."""
result = await git_wrapper_with_repo.create_branch("feature-test")
assert result.success is True
assert result.branch == "feature-test"
assert result.is_current is True
@pytest.mark.asyncio
async def test_create_branch_without_checkout(self, git_wrapper_with_repo):
"""Test creating branch without checkout."""
result = await git_wrapper_with_repo.create_branch(
"feature-no-checkout", checkout=False
)
assert result.success is True
assert result.branch == "feature-no-checkout"
assert result.is_current is False
@pytest.mark.asyncio
async def test_create_branch_exists_error(self, git_wrapper_with_repo):
"""Test error when branch already exists."""
await git_wrapper_with_repo.create_branch("existing-branch", checkout=False)
with pytest.raises(BranchExistsError):
await git_wrapper_with_repo.create_branch("existing-branch")
@pytest.mark.asyncio
async def test_delete_branch(self, git_wrapper_with_repo):
"""Test deleting a branch."""
# Create branch first
await git_wrapper_with_repo.create_branch("to-delete", checkout=False)
# Delete it
result = await git_wrapper_with_repo.delete_branch("to-delete")
assert result.success is True
assert result.branch == "to-delete"
@pytest.mark.asyncio
async def test_delete_branch_not_found(self, git_wrapper_with_repo):
"""Test error when deleting non-existent branch."""
with pytest.raises(BranchNotFoundError):
await git_wrapper_with_repo.delete_branch("nonexistent")
@pytest.mark.asyncio
async def test_delete_current_branch_error(self, git_wrapper_with_repo):
"""Test error when deleting current branch."""
with pytest.raises(GitError, match="Cannot delete current branch"):
await git_wrapper_with_repo.delete_branch("main")
@pytest.mark.asyncio
async def test_list_branches(self, git_wrapper_with_repo):
"""Test listing branches."""
# Create some branches
await git_wrapper_with_repo.create_branch("branch-a", checkout=False)
await git_wrapper_with_repo.create_branch("branch-b", checkout=False)
result = await git_wrapper_with_repo.list_branches()
assert result.current_branch == "main"
branch_names = [b["name"] for b in result.local_branches]
assert "main" in branch_names
assert "branch-a" in branch_names
assert "branch-b" in branch_names
class TestGitWrapperCheckout:
"""Tests for checkout operations."""
@pytest.mark.asyncio
async def test_checkout_existing_branch(self, git_wrapper_with_repo):
"""Test checkout of existing branch."""
# Create branch first
await git_wrapper_with_repo.create_branch("test-branch", checkout=False)
result = await git_wrapper_with_repo.checkout("test-branch")
assert result.success is True
assert result.ref == "test-branch"
@pytest.mark.asyncio
async def test_checkout_create_new(self, git_wrapper_with_repo):
"""Test checkout with branch creation."""
result = await git_wrapper_with_repo.checkout("new-branch", create_branch=True)
assert result.success is True
assert result.ref == "new-branch"
@pytest.mark.asyncio
async def test_checkout_nonexistent_error(self, git_wrapper_with_repo):
"""Test error when checking out non-existent ref."""
with pytest.raises(CheckoutError):
await git_wrapper_with_repo.checkout("nonexistent-branch")
class TestGitWrapperCommit:
"""Tests for commit operations."""
@pytest.mark.asyncio
async def test_commit_staged_changes(self, git_wrapper_with_repo, git_repo):
"""Test committing staged changes."""
# Create and stage a file
new_file = Path(git_repo.working_dir) / "newfile.txt"
new_file.write_text("new content")
git_repo.index.add(["newfile.txt"])
result = await git_wrapper_with_repo.commit("Add new file")
assert result.success is True
assert result.message == "Add new file"
assert result.files_changed == 1
@pytest.mark.asyncio
async def test_commit_all_changes(self, git_wrapper_with_repo, git_repo):
"""Test committing all changes (auto-stage)."""
# Create a file without staging
new_file = Path(git_repo.working_dir) / "unstaged.txt"
new_file.write_text("content")
result = await git_wrapper_with_repo.commit("Commit unstaged")
assert result.success is True
@pytest.mark.asyncio
async def test_commit_nothing_to_commit(self, git_wrapper_with_repo):
"""Test error when nothing to commit."""
with pytest.raises(CommitError, match="Nothing to commit"):
await git_wrapper_with_repo.commit("Empty commit")
@pytest.mark.asyncio
async def test_commit_with_author(self, git_wrapper_with_repo, git_repo):
"""Test commit with custom author."""
new_file = Path(git_repo.working_dir) / "authored.txt"
new_file.write_text("authored content")
result = await git_wrapper_with_repo.commit(
"Custom author commit",
author_name="Custom Author",
author_email="custom@test.com",
)
assert result.success is True
class TestGitWrapperDiff:
"""Tests for diff operations."""
@pytest.mark.asyncio
async def test_diff_no_changes(self, git_wrapper_with_repo):
"""Test diff with no changes."""
result = await git_wrapper_with_repo.diff()
assert result.files_changed == 0
assert result.total_additions == 0
assert result.total_deletions == 0
@pytest.mark.asyncio
async def test_diff_with_changes(self, git_wrapper_with_repo, git_repo):
"""Test diff with modified files."""
# Modify a file
readme = Path(git_repo.working_dir) / "README.md"
readme.write_text("# Modified\nNew line\n")
result = await git_wrapper_with_repo.diff()
assert result.files_changed > 0
class TestGitWrapperLog:
"""Tests for log operations."""
@pytest.mark.asyncio
async def test_log_basic(self, git_wrapper_with_repo):
"""Test basic log."""
result = await git_wrapper_with_repo.log()
assert result.total_commits > 0
assert len(result.commits) > 0
@pytest.mark.asyncio
async def test_log_with_limit(self, git_wrapper_with_repo, git_repo):
"""Test log with limit."""
# Create more commits
for i in range(5):
file_path = Path(git_repo.working_dir) / f"file{i}.txt"
file_path.write_text(f"content {i}")
git_repo.index.add([f"file{i}.txt"])
git_repo.index.commit(f"Commit {i}")
result = await git_wrapper_with_repo.log(limit=3)
assert len(result.commits) == 3
@pytest.mark.asyncio
async def test_log_commit_info(self, git_wrapper_with_repo):
"""Test that log returns proper commit info."""
result = await git_wrapper_with_repo.log(limit=1)
commit = result.commits[0]
assert "sha" in commit
assert "message" in commit
assert "author_name" in commit
assert "author_email" in commit
class TestGitWrapperUtilities:
"""Tests for utility methods."""
@pytest.mark.asyncio
async def test_is_valid_ref_true(self, git_wrapper_with_repo):
"""Test valid ref detection."""
is_valid = await git_wrapper_with_repo.is_valid_ref("main")
assert is_valid is True
@pytest.mark.asyncio
async def test_is_valid_ref_false(self, git_wrapper_with_repo):
"""Test invalid ref detection."""
is_valid = await git_wrapper_with_repo.is_valid_ref("nonexistent")
assert is_valid is False
def test_diff_to_change_type(self, git_wrapper_with_repo):
"""Test change type conversion."""
wrapper = git_wrapper_with_repo
assert wrapper._diff_to_change_type("A") == FileChangeType.ADDED
assert wrapper._diff_to_change_type("M") == FileChangeType.MODIFIED
assert wrapper._diff_to_change_type("D") == FileChangeType.DELETED
assert wrapper._diff_to_change_type("R") == FileChangeType.RENAMED
class TestGitWrapperStage:
"""Tests for staging operations."""
@pytest.mark.asyncio
async def test_stage_specific_files(self, git_wrapper_with_repo, git_repo):
"""Test staging specific files."""
# Create files
file1 = Path(git_repo.working_dir) / "file1.txt"
file2 = Path(git_repo.working_dir) / "file2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
count = await git_wrapper_with_repo.stage(["file1.txt"])
assert count == 1
@pytest.mark.asyncio
async def test_stage_all(self, git_wrapper_with_repo, git_repo):
"""Test staging all files."""
file1 = Path(git_repo.working_dir) / "all1.txt"
file2 = Path(git_repo.working_dir) / "all2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
count = await git_wrapper_with_repo.stage()
assert count >= 2
@pytest.mark.asyncio
async def test_unstage_files(self, git_wrapper_with_repo, git_repo):
"""Test unstaging files."""
# Create and stage file
file1 = Path(git_repo.working_dir) / "unstage.txt"
file1.write_text("to unstage")
git_repo.index.add(["unstage.txt"])
count = await git_wrapper_with_repo.unstage()
assert count >= 1
class TestGitWrapperReset:
"""Tests for reset operations."""
@pytest.mark.asyncio
async def test_reset_soft(self, git_wrapper_with_repo, git_repo):
"""Test soft reset."""
# Create a commit to reset
file1 = Path(git_repo.working_dir) / "reset_soft.txt"
file1.write_text("content")
git_repo.index.add(["reset_soft.txt"])
git_repo.index.commit("Commit to reset")
result = await git_wrapper_with_repo.reset("HEAD~1", mode="soft")
assert result is True
@pytest.mark.asyncio
async def test_reset_mixed(self, git_wrapper_with_repo, git_repo):
"""Test mixed reset (default)."""
file1 = Path(git_repo.working_dir) / "reset_mixed.txt"
file1.write_text("content")
git_repo.index.add(["reset_mixed.txt"])
git_repo.index.commit("Commit to reset")
result = await git_wrapper_with_repo.reset("HEAD~1", mode="mixed")
assert result is True
@pytest.mark.asyncio
async def test_reset_invalid_mode(self, git_wrapper_with_repo):
"""Test error on invalid reset mode."""
with pytest.raises(GitError, match="Invalid reset mode"):
await git_wrapper_with_repo.reset("HEAD", mode="invalid")
class TestGitWrapperStash:
"""Tests for stash operations."""
@pytest.mark.asyncio
async def test_stash_changes(self, git_wrapper_with_repo, git_repo):
"""Test stashing changes."""
# Make changes
readme = Path(git_repo.working_dir) / "README.md"
readme.write_text("Modified for stash")
result = await git_wrapper_with_repo.stash("Test stash")
# Result should be stash ref or None if nothing to stash
# (depends on whether changes were already staged)
assert result is None or result.startswith("stash@")
@pytest.mark.asyncio
async def test_stash_nothing(self, git_wrapper_with_repo):
"""Test stash with no changes."""
result = await git_wrapper_with_repo.stash()
assert result is None
@pytest.mark.asyncio
async def test_stash_pop(self, git_wrapper_with_repo, git_repo):
"""Test popping a stash."""
# Make changes and stash them
readme = Path(git_repo.working_dir) / "README.md"
original_content = readme.read_text()
readme.write_text("Modified for stash pop test")
git_repo.index.add(["README.md"])
stash_ref = await git_wrapper_with_repo.stash("Test stash for pop")
if stash_ref:
# Pop the stash
result = await git_wrapper_with_repo.stash_pop()
assert result is True
class TestGitWrapperRepoProperty:
"""Tests for repo property edge cases."""
def test_repo_property_path_not_exists(self, test_settings):
"""Test that accessing repo on non-existent path raises error."""
wrapper = GitWrapper(
Path("/nonexistent/path/that/does/not/exist"), test_settings
)
with pytest.raises(GitError, match="Path does not exist"):
_ = wrapper.repo
def test_refresh_repo(self, git_wrapper_with_repo):
"""Test _refresh_repo clears cached repo."""
# Access repo to cache it
_ = git_wrapper_with_repo.repo
assert git_wrapper_with_repo._repo is not None
# Refresh should clear it
git_wrapper_with_repo._refresh_repo()
assert git_wrapper_with_repo._repo is None
class TestGitWrapperBranchAdvanced:
"""Advanced tests for branch operations."""
@pytest.mark.asyncio
async def test_create_branch_from_ref(self, git_wrapper_with_repo, git_repo):
"""Test creating branch from specific ref."""
# Get current HEAD SHA
head_sha = git_repo.head.commit.hexsha
result = await git_wrapper_with_repo.create_branch(
"feature-from-ref",
from_ref=head_sha,
checkout=False,
)
assert result.success is True
assert result.branch == "feature-from-ref"
@pytest.mark.asyncio
async def test_delete_branch_force(self, git_wrapper_with_repo, git_repo):
"""Test force deleting a branch."""
# Create branch and add unmerged commit
await git_wrapper_with_repo.create_branch("unmerged-branch", checkout=True)
new_file = Path(git_repo.working_dir) / "unmerged.txt"
new_file.write_text("unmerged content")
git_repo.index.add(["unmerged.txt"])
git_repo.index.commit("Unmerged commit")
# Switch back to main
await git_wrapper_with_repo.checkout("main")
# Force delete
result = await git_wrapper_with_repo.delete_branch(
"unmerged-branch", force=True
)
assert result.success is True
class TestGitWrapperListBranchesRemote:
"""Tests for listing remote branches."""
@pytest.mark.asyncio
async def test_list_branches_with_remote(self, git_wrapper_with_repo):
"""Test listing branches including remote."""
# Even without remotes, this should work
result = await git_wrapper_with_repo.list_branches(include_remote=True)
assert result.current_branch == "main"
# Remote branches list should be empty for local repo
assert len(result.remote_branches) == 0
class TestGitWrapperCheckoutAdvanced:
"""Advanced tests for checkout operations."""
@pytest.mark.asyncio
async def test_checkout_create_existing_error(self, git_wrapper_with_repo):
"""Test error when creating branch that already exists."""
with pytest.raises(BranchExistsError):
await git_wrapper_with_repo.checkout("main", create_branch=True)
@pytest.mark.asyncio
async def test_checkout_force(self, git_wrapper_with_repo, git_repo):
"""Test force checkout discards local changes."""
# Create branch
await git_wrapper_with_repo.create_branch("force-test", checkout=False)
# Make local changes
readme = Path(git_repo.working_dir) / "README.md"
readme.write_text("local changes")
# Force checkout should work
result = await git_wrapper_with_repo.checkout("force-test", force=True)
assert result.success is True
class TestGitWrapperCommitAdvanced:
"""Advanced tests for commit operations."""
@pytest.mark.asyncio
async def test_commit_specific_files(self, git_wrapper_with_repo, git_repo):
"""Test committing specific files only."""
# Create multiple files
file1 = Path(git_repo.working_dir) / "commit_specific1.txt"
file2 = Path(git_repo.working_dir) / "commit_specific2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
result = await git_wrapper_with_repo.commit(
"Commit specific file",
files=["commit_specific1.txt"],
)
assert result.success is True
assert result.files_changed == 1
@pytest.mark.asyncio
async def test_commit_with_partial_author(self, git_wrapper_with_repo, git_repo):
"""Test commit with only author name."""
new_file = Path(git_repo.working_dir) / "partial_author.txt"
new_file.write_text("content")
result = await git_wrapper_with_repo.commit(
"Partial author commit",
author_name="Test Author",
)
assert result.success is True
@pytest.mark.asyncio
async def test_commit_allow_empty(self, git_wrapper_with_repo):
"""Test allowing empty commits."""
result = await git_wrapper_with_repo.commit(
"Empty commit allowed",
allow_empty=True,
)
assert result.success is True
class TestGitWrapperUnstageAdvanced:
"""Advanced tests for unstaging operations."""
@pytest.mark.asyncio
async def test_unstage_specific_files(self, git_wrapper_with_repo, git_repo):
"""Test unstaging specific files."""
# Create and stage files
file1 = Path(git_repo.working_dir) / "unstage1.txt"
file2 = Path(git_repo.working_dir) / "unstage2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
git_repo.index.add(["unstage1.txt", "unstage2.txt"])
count = await git_wrapper_with_repo.unstage(["unstage1.txt"])
assert count == 1
class TestGitWrapperResetAdvanced:
"""Advanced tests for reset operations."""
@pytest.mark.asyncio
async def test_reset_hard(self, git_wrapper_with_repo, git_repo):
"""Test hard reset."""
# Create a commit
file1 = Path(git_repo.working_dir) / "reset_hard.txt"
file1.write_text("content")
git_repo.index.add(["reset_hard.txt"])
git_repo.index.commit("Commit for hard reset")
result = await git_wrapper_with_repo.reset("HEAD~1", mode="hard")
assert result is True
# File should be gone after hard reset
assert not file1.exists()
@pytest.mark.asyncio
async def test_reset_specific_files(self, git_wrapper_with_repo, git_repo):
"""Test resetting specific files."""
# Create and stage a file
file1 = Path(git_repo.working_dir) / "reset_file.txt"
file1.write_text("content")
git_repo.index.add(["reset_file.txt"])
result = await git_wrapper_with_repo.reset("HEAD", files=["reset_file.txt"])
assert result is True
class TestGitWrapperDiffAdvanced:
"""Advanced tests for diff operations."""
@pytest.mark.asyncio
async def test_diff_between_refs(self, git_wrapper_with_repo, git_repo):
"""Test diff between two refs."""
# Create initial commit
file1 = Path(git_repo.working_dir) / "diff_ref.txt"
file1.write_text("initial")
git_repo.index.add(["diff_ref.txt"])
commit1 = git_repo.index.commit("First commit for diff")
# Create second commit
file1.write_text("modified")
git_repo.index.add(["diff_ref.txt"])
commit2 = git_repo.index.commit("Second commit for diff")
result = await git_wrapper_with_repo.diff(
base=commit1.hexsha,
head=commit2.hexsha,
)
assert result.files_changed > 0
@pytest.mark.asyncio
async def test_diff_specific_files(self, git_wrapper_with_repo, git_repo):
"""Test diff for specific files only."""
# Create files
file1 = Path(git_repo.working_dir) / "diff_specific1.txt"
file2 = Path(git_repo.working_dir) / "diff_specific2.txt"
file1.write_text("content 1")
file2.write_text("content 2")
result = await git_wrapper_with_repo.diff(files=["diff_specific1.txt"])
# Should only show changes for specified file
for f in result.files:
assert "diff_specific2.txt" not in f.get("path", "")
@pytest.mark.asyncio
async def test_diff_base_only(self, git_wrapper_with_repo, git_repo):
"""Test diff with base ref only (vs HEAD)."""
# Create commit
file1 = Path(git_repo.working_dir) / "diff_base.txt"
file1.write_text("content")
git_repo.index.add(["diff_base.txt"])
commit = git_repo.index.commit("Commit for diff base test")
# Get parent commit
parent = commit.parents[0] if commit.parents else commit
result = await git_wrapper_with_repo.diff(base=parent.hexsha)
assert isinstance(result.files_changed, int)
class TestGitWrapperLogAdvanced:
"""Advanced tests for log operations."""
@pytest.mark.asyncio
async def test_log_with_ref(self, git_wrapper_with_repo, git_repo):
"""Test log starting from specific ref."""
# Create branch with commits
await git_wrapper_with_repo.create_branch("log-test", checkout=True)
file1 = Path(git_repo.working_dir) / "log_ref.txt"
file1.write_text("content")
git_repo.index.add(["log_ref.txt"])
git_repo.index.commit("Commit on log-test branch")
result = await git_wrapper_with_repo.log(ref="log-test", limit=5)
assert result.total_commits > 0
@pytest.mark.asyncio
async def test_log_with_path(self, git_wrapper_with_repo, git_repo):
"""Test log filtered by path."""
# Create file and commit
file1 = Path(git_repo.working_dir) / "log_path.txt"
file1.write_text("content")
git_repo.index.add(["log_path.txt"])
git_repo.index.commit("Commit for path log")
result = await git_wrapper_with_repo.log(path="log_path.txt")
assert result.total_commits >= 1
@pytest.mark.asyncio
async def test_log_with_skip(self, git_wrapper_with_repo, git_repo):
"""Test log with skip parameter."""
# Create multiple commits
for i in range(3):
file_path = Path(git_repo.working_dir) / f"skip_test{i}.txt"
file_path.write_text(f"content {i}")
git_repo.index.add([f"skip_test{i}.txt"])
git_repo.index.commit(f"Skip test commit {i}")
result = await git_wrapper_with_repo.log(skip=1, limit=2)
# Should have skipped first commit
assert len(result.commits) <= 2
class TestGitWrapperRemoteUrl:
"""Tests for remote URL operations."""
@pytest.mark.asyncio
async def test_get_remote_url_nonexistent(self, git_wrapper_with_repo):
"""Test getting URL for non-existent remote."""
url = await git_wrapper_with_repo.get_remote_url("nonexistent")
assert url is None
class TestGitWrapperConfig:
"""Tests for git config operations."""
@pytest.mark.asyncio
async def test_set_and_get_config(self, git_wrapper_with_repo):
"""Test setting and getting config value."""
await git_wrapper_with_repo.set_config("test.key", "test_value")
value = await git_wrapper_with_repo.get_config("test.key")
assert value == "test_value"
@pytest.mark.asyncio
async def test_get_config_nonexistent(self, git_wrapper_with_repo):
"""Test getting non-existent config value."""
value = await git_wrapper_with_repo.get_config("nonexistent.key")
assert value is None
class TestGitWrapperClone:
"""Tests for clone operations."""
@pytest.mark.asyncio
async def test_clone_success(self, temp_workspace, test_settings):
"""Test successful clone."""
wrapper = GitWrapper(temp_workspace, test_settings)
# Mock the clone operation
with patch("git_wrapper.GitRepo") as mock_repo_class:
mock_repo = MagicMock()
mock_repo.active_branch.name = "main"
mock_repo.head.commit.hexsha = "abc123"
mock_repo_class.clone_from.return_value = mock_repo
result = await wrapper.clone("https://github.com/test/repo.git")
assert result.success is True
assert result.branch == "main"
assert result.commit_sha == "abc123"
@pytest.mark.asyncio
async def test_clone_with_auth_token(self, temp_workspace, test_settings):
"""Test clone with auth token."""
wrapper = GitWrapper(temp_workspace, test_settings)
with patch("git_wrapper.GitRepo") as mock_repo_class:
mock_repo = MagicMock()
mock_repo.active_branch.name = "main"
mock_repo.head.commit.hexsha = "abc123"
mock_repo_class.clone_from.return_value = mock_repo
result = await wrapper.clone(
"https://github.com/test/repo.git",
auth_token="test-token",
)
assert result.success is True
# Verify token was injected in URL
call_args = mock_repo_class.clone_from.call_args
assert "test-token@" in call_args.kwargs["url"]
@pytest.mark.asyncio
async def test_clone_with_branch_and_depth(self, temp_workspace, test_settings):
"""Test clone with branch and depth parameters."""
wrapper = GitWrapper(temp_workspace, test_settings)
with patch("git_wrapper.GitRepo") as mock_repo_class:
mock_repo = MagicMock()
mock_repo.active_branch.name = "develop"
mock_repo.head.commit.hexsha = "def456"
mock_repo_class.clone_from.return_value = mock_repo
result = await wrapper.clone(
"https://github.com/test/repo.git",
branch="develop",
depth=1,
)
assert result.success is True
call_args = mock_repo_class.clone_from.call_args
assert call_args.kwargs["branch"] == "develop"
assert call_args.kwargs["depth"] == 1
@pytest.mark.asyncio
async def test_clone_failure(self, temp_workspace, test_settings):
"""Test clone failure raises CloneError."""
wrapper = GitWrapper(temp_workspace, test_settings)
with patch("git_wrapper.GitRepo") as mock_repo_class:
mock_repo_class.clone_from.side_effect = GitCommandError(
"git clone", 128, stderr="Authentication failed"
)
with pytest.raises(CloneError):
await wrapper.clone("https://github.com/test/repo.git")
class TestGitWrapperPush:
"""Tests for push operations."""
@pytest.mark.asyncio
async def test_push_force_disabled(self, git_wrapper_with_repo, git_repo):
"""Test force push is disabled by default."""
git_repo.create_remote("origin", "https://github.com/test/repo.git")
with pytest.raises(PushError, match="Force push is disabled"):
await git_wrapper_with_repo.push(force=True)
@pytest.mark.asyncio
async def test_push_remote_not_found(self, git_wrapper_with_repo):
"""Test push to non-existent remote."""
with pytest.raises(PushError, match="Remote not found"):
await git_wrapper_with_repo.push(remote="nonexistent")
class TestGitWrapperPull:
"""Tests for pull operations."""
@pytest.mark.asyncio
async def test_pull_remote_not_found(self, git_wrapper_with_repo):
"""Test pull from non-existent remote."""
with pytest.raises(PullError, match="Remote not found"):
await git_wrapper_with_repo.pull(remote="nonexistent")
class TestGitWrapperFetch:
"""Tests for fetch operations."""
@pytest.mark.asyncio
async def test_fetch_remote_not_found(self, git_wrapper_with_repo):
"""Test fetch from non-existent remote."""
with pytest.raises(GitError, match="Remote not found"):
await git_wrapper_with_repo.fetch(remote="nonexistent")
class TestGitWrapperDiffHeadOnly:
"""Tests for diff with head ref only."""
@pytest.mark.asyncio
async def test_diff_head_only(self, git_wrapper_with_repo, git_repo):
"""Test diff with head ref only (working tree vs ref)."""
# Make some changes
readme = Path(git_repo.working_dir) / "README.md"
readme.write_text("modified content")
# This tests the head-only branch (base=None, head=specified)
result = await git_wrapper_with_repo.diff(head="HEAD")
assert isinstance(result.files_changed, int)
class TestGitWrapperRemoteWithUrl:
"""Tests for getting remote URL when remote exists."""
@pytest.mark.asyncio
async def test_get_remote_url_exists(self, git_wrapper_with_repo, git_repo):
"""Test getting URL for existing remote."""
git_repo.create_remote("origin", "https://github.com/test/repo.git")
url = await git_wrapper_with_repo.get_remote_url("origin")
assert url == "https://github.com/test/repo.git"
class TestRunInExecutor:
"""Tests for run_in_executor utility."""
@pytest.mark.asyncio
async def test_run_in_executor(self):
"""Test running function in executor."""
def blocking_func(x, y):
return x + y
result = await run_in_executor(blocking_func, 1, 2)
assert result == 3