""" 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