# #region Test.Git.Merge.Coverage [C:3] [TYPE Module] [SEMANTICS test, git, merge, coverage, edge] # @BRIEF Additional edge coverage for GitServiceMergeMixin — error-specific paths in _build_unfinished_merge_payload, resolve_merge_conflicts root, promote_direct_merge fallbacks. # @RELATION BINDS_TO -> [GitServiceMergeMixin] import contextlib import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src")) from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock import pytest from fastapi import HTTPException from git.exc import GitCommandError @pytest.fixture def mixin(): from src.services.git._merge import GitServiceMergeMixin m = GitServiceMergeMixin() m._locked = lambda x: contextlib.nullcontext() return m def _make_ref(name: str): """Create a simple mock object with .name returning the given string.""" r = MagicMock() r.name = name return r class TestBuildUnfinishedMergePayload: """Cover edge-error handlers in _build_unfinished_merge_payload.""" def test_merge_head_read_error(self, mixin): """merge_head_path.read_text fails → merge_head = ''.""" repo = MagicMock() repo.git_dir = "/tmp/repo/.git" repo.working_tree_dir = "/tmp/repo" repo.active_branch.name = "main" repo.index.unmerged_blobs.return_value = {} with patch("os.path.exists", return_value=True), \ patch("pathlib.Path.read_text", side_effect=[OSError("perm denied"), "merge msg"]): result = mixin._build_unfinished_merge_payload(repo) assert result["merge_head"] == "" def test_merge_msg_read_error(self, mixin): """MERGE_MSG read fails → merge_message_preview = ''.""" repo = MagicMock() repo.git_dir = "/tmp/repo/.git" repo.working_tree_dir = "/tmp/repo" repo.active_branch.name = "main" repo.index.unmerged_blobs.return_value = {} with patch("os.path.exists", return_value=True), \ patch("pathlib.Path.read_text", side_effect=["abc123", OSError("no msg")]): result = mixin._build_unfinished_merge_payload(repo) assert result["merge_message_preview"] == "" def test_merge_msg_file_missing(self, mixin): """MERGE_MSG file does not exist → merge_message_preview = empty.""" repo = MagicMock() repo.git_dir = "/tmp/repo/.git" repo.working_tree_dir = "/tmp/repo" repo.active_branch.name = "main" repo.index.unmerged_blobs.return_value = {} def exists_side(path): return "MERGE_MSG" not in path with patch("os.path.exists", side_effect=exists_side), \ patch("pathlib.Path.read_text", return_value="abc123"): result = mixin._build_unfinished_merge_payload(repo) assert result["merge_message_preview"] == "" def test_active_branch_error(self, mixin): """active_branch.name raises → current_branch = 'detached_or_unknown'.""" repo = MagicMock() repo.git_dir = "/tmp/repo/.git" repo.working_tree_dir = "/tmp/repo" type(repo.active_branch).name = PropertyMock(side_effect=Exception("detached")) repo.index.unmerged_blobs.return_value = {} with patch("os.path.exists", return_value=True), \ patch("pathlib.Path.read_text", return_value="abc123"): result = mixin._build_unfinished_merge_payload(repo) assert result["current_branch"] == "detached_or_unknown" class TestResolveMergeConflicts: """Cover missing root error path.""" def test_empty_working_tree_dir_raises(self, mixin): """Empty string working_tree_dir → repo_root becomes CWD, but abspath returns ''.""" repo = MagicMock() repo.working_tree_dir = "" with patch.object(mixin, 'get_repo', return_value=repo, create=True), \ patch("os.path.abspath", return_value=""): with pytest.raises(HTTPException, match="unavailable"): mixin.resolve_merge_conflicts(1, [{"file_path": "f.txt", "resolution": "mine"}]) class TestPromoteDirectMerge: """Cover promote error-recovery paths.""" def test_original_branch_exception(self, mixin): """active_branch.name raises → original_branch = None.""" repo = MagicMock() repo.heads = [_make_ref("dev"), _make_ref("main")] repo.refs = [_make_ref("origin/dev"), _make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin type(repo.active_branch).name = PropertyMock(side_effect=[Exception("no branch"), "dev"]) with patch.object(mixin, 'get_repo', return_value=repo, create=True): result = mixin.promote_direct_merge(1, "dev", "main") assert result["status"] == "merged" def test_source_branch_from_origin_ref(self, mixin): """Source from origin/ref → checkout -b from origin.""" repo = MagicMock() repo.heads = [_make_ref("main")] repo.refs = [_make_ref("origin/dev"), _make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): result = mixin.promote_direct_merge(1, "dev", "main") assert result["status"] == "merged" repo.git.checkout.assert_any_call("-b", "dev", "origin/dev") def test_target_branch_from_origin_ref(self, mixin): """Target from origin/ref → checkout -b from origin.""" repo = MagicMock() repo.heads = [_make_ref("dev")] repo.refs = [_make_ref("origin/dev"), _make_ref("origin/staging")] origin = MagicMock() repo.remote.return_value = origin type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): result = mixin.promote_direct_merge(1, "dev", "staging") assert result["status"] == "merged" def test_pull_target_failure_logged(self, mixin): """origin.pull(target) fails → logged, merge proceeds.""" repo = MagicMock() repo.heads = [_make_ref("dev"), _make_ref("main")] repo.refs = [_make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): result = mixin.promote_direct_merge(1, "dev", "main") assert result["status"] == "merged" def test_merge_conflict_raises_409(self, mixin): """CONFLICT → HTTP 409.""" repo = MagicMock() repo.heads = [_make_ref("dev"), _make_ref("main")] repo.refs = [_make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin repo.git.merge.side_effect = Exception("CONFLICT in file.txt") type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): with pytest.raises(HTTPException, match="Merge conflict"): mixin.promote_direct_merge(1, "dev", "main") def test_merge_generic_error_raises_500(self, mixin): """Generic error → HTTP 500.""" repo = MagicMock() repo.heads = [_make_ref("dev"), _make_ref("main")] repo.refs = [_make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin repo.git.merge.side_effect = Exception("some other error") type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): with pytest.raises(HTTPException, match="Direct promote failed"): mixin.promote_direct_merge(1, "dev", "main") def test_restore_original_branch_failure(self, mixin): """Restore fails in finally → caught.""" repo = MagicMock() repo.heads = [_make_ref("dev"), _make_ref("main")] repo.refs = [_make_ref("origin/main")] origin = MagicMock() repo.remote.return_value = origin repo.git.checkout.side_effect = [None, None, GeneratorExit("checkout fail")] type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): result = mixin.promote_direct_merge(1, "dev", "main") assert result["status"] == "merged" def test_target_branch_not_found(self, mixin): """Target not found → HTTP 404.""" repo = MagicMock() repo.heads = [_make_ref("dev")] repo.refs = [_make_ref("origin/dev")] origin = MagicMock() repo.remote.return_value = origin type(repo.active_branch).name = PropertyMock(return_value="dev") with patch.object(mixin, 'get_repo', return_value=repo, create=True): with pytest.raises(HTTPException, match="not found"): mixin.promote_direct_merge(1, "dev", "nonexistent") # #endregion Test.Git.Merge.Coverage