vibe/tests/core/test_teleport_git.py
Mathias Gesbert 51fecc67d9
2.1.0 (#317)
Co-authored-by: Quentin Torroba <quentin.torroba@mistral.ai>
Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai>
Co-authored-by: Clément Siriex <clement.sirieix@mistral.ai>
Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai>
Co-authored-by: Nicolas Karolak <nicolas@karolak.fr>
2026-02-11 18:17:30 +01:00

327 lines
12 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from vibe.core.teleport.errors import (
ServiceTeleportError,
ServiceTeleportNotSupportedError,
)
from vibe.core.teleport.git import GitRepoInfo, GitRepository
def make_mock_remote(url: str) -> MagicMock:
remote = MagicMock()
remote.urls = [url]
return remote
def make_mock_repo(
urls: list[str] | None = None,
commit: str | None = "abc123",
branch: str | None = "main",
is_detached: bool = False,
diff: str = "",
) -> MagicMock:
mock = MagicMock()
if urls:
mock.remotes = [make_mock_remote(url) for url in urls]
else:
mock.remotes = []
mock.head.commit.hexsha = commit
mock.head.is_detached = is_detached
mock.active_branch.name = branch
mock.git.diff.return_value = diff
mock.git.branch.return_value = ""
mock.git.rev_list.return_value = "0"
mock.git.rev_parse.return_value = "abc123"
mock.remote.return_value = make_mock_remote(urls[0] if urls else "")
return mock
class TestGitRepositoryParseGithubUrl:
def test_parse_ssh_url(self) -> None:
result = GitRepository._parse_github_url("git@github.com:owner/repo.git")
assert result == ("owner", "repo")
def test_parse_ssh_url_without_git_suffix(self) -> None:
result = GitRepository._parse_github_url("git@github.com:owner/repo")
assert result == ("owner", "repo")
def test_parse_https_url(self) -> None:
result = GitRepository._parse_github_url("https://github.com/owner/repo.git")
assert result == ("owner", "repo")
def test_parse_https_url_without_git_suffix(self) -> None:
result = GitRepository._parse_github_url("https://github.com/owner/repo")
assert result == ("owner", "repo")
def test_parse_https_url_with_credentials(self) -> None:
result = GitRepository._parse_github_url(
"https://x-access-token:gho_xxxx@github.com/owner/repo.git"
)
assert result == ("owner", "repo")
def test_parse_non_github_url_returns_none(self) -> None:
result = GitRepository._parse_github_url("git@gitlab.com:owner/repo.git")
assert result is None
def test_parse_invalid_url_returns_none(self) -> None:
result = GitRepository._parse_github_url("not-a-valid-url")
assert result is None
class TestGitRepositoryToHttpsUrl:
def test_converts_to_https_url(self) -> None:
result = GitRepository._to_https_url("owner", "repo")
assert result == "https://github.com/owner/repo.git"
class TestGitRepositoryIsSupported:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_returns_false_when_not_git_repo(self, tmp_path: Path) -> None:
repo = GitRepository(tmp_path)
assert await repo.is_supported() is False
@pytest.mark.asyncio
async def test_returns_false_when_no_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=None)
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_supported() is False
@pytest.mark.asyncio
async def test_returns_false_when_non_github_remote(
self, repo: GitRepository
) -> None:
mock = make_mock_repo(urls=["git@gitlab.com:owner/repo.git"])
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_supported() is False
@pytest.mark.asyncio
async def test_returns_true_when_github_repo(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"])
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_supported() is True
@pytest.mark.asyncio
async def test_finds_github_among_multiple_remotes(
self, repo: GitRepository
) -> None:
mock = make_mock_repo(
urls=["git@gitlab.com:owner/repo.git", "git@github.com:owner/repo.git"]
)
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_supported() is True
class TestGitRepositoryGetInfo:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_raises_when_not_git_repo(self, tmp_path: Path) -> None:
repo = GitRepository(tmp_path)
with pytest.raises(
ServiceTeleportNotSupportedError, match="Not a git repository"
):
await repo.get_info()
@pytest.mark.asyncio
async def test_raises_when_no_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=None)
with patch.object(repo, "_repo_or_raise", return_value=mock):
with pytest.raises(
ServiceTeleportNotSupportedError, match="No GitHub remote"
):
await repo.get_info()
@pytest.mark.asyncio
async def test_raises_when_non_github_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@gitlab.com:owner/repo.git"])
with patch.object(repo, "_repo_or_raise", return_value=mock):
with pytest.raises(
ServiceTeleportNotSupportedError, match="No GitHub remote"
):
await repo.get_info()
@pytest.mark.asyncio
async def test_raises_when_no_commit(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], commit=None)
with patch.object(repo, "_repo_or_raise", return_value=mock):
with pytest.raises(
ServiceTeleportNotSupportedError,
match="Could not determine current commit",
):
await repo.get_info()
@pytest.mark.asyncio
async def test_returns_info_on_success(self, repo: GitRepository) -> None:
mock = make_mock_repo(
urls=["git@github.com:owner/repo.git"],
commit="abc123def456",
branch="main",
diff="diff content",
)
with patch.object(repo, "_repo_or_raise", return_value=mock):
info = await repo.get_info()
assert info == GitRepoInfo(
remote_url="https://github.com/owner/repo.git",
owner="owner",
repo="repo",
branch="main",
commit="abc123def456",
diff="diff content",
)
@pytest.mark.asyncio
async def test_handles_detached_head(self, repo: GitRepository) -> None:
mock = make_mock_repo(
urls=["git@github.com:owner/repo.git"],
commit="abc123def456",
is_detached=True,
)
with patch.object(repo, "_repo_or_raise", return_value=mock):
info = await repo.get_info()
assert info.branch is None
class TestGitRepositoryIsCommitPushed:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_returns_false_when_not_on_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"])
mock.git.branch.return_value = ""
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_commit_pushed("abc123") is False
@pytest.mark.asyncio
async def test_returns_true_when_on_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"])
mock.git.branch.return_value = " origin/main\n origin/feature"
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_commit_pushed("abc123") is True
@pytest.mark.asyncio
async def test_checks_correct_remote(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"])
mock.git.branch.return_value = " upstream/main\n"
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.is_commit_pushed("abc123", remote="upstream") is True
assert await repo.is_commit_pushed("abc123", remote="origin") is False
class TestGitRepositoryGetUnpushedCommitCount:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_raises_when_no_branch(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], is_detached=True)
with patch.object(repo, "_repo_or_raise", return_value=mock):
with pytest.raises(ServiceTeleportError, match="no current branch"):
await repo.get_unpushed_commit_count()
@pytest.mark.asyncio
async def test_returns_count(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], branch="main")
mock.git.rev_list.return_value = "5"
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.get_unpushed_commit_count() == 5
@pytest.mark.asyncio
async def test_fallback_to_default_branch(self, repo: GitRepository) -> None:
from git.exc import GitCommandError
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], branch="feature")
def rev_list_side_effect(*args: object, **kwargs: object) -> str:
if "origin/feature..HEAD" in args:
raise GitCommandError("git rev-list", 128)
if "origin/main..HEAD" in args:
return "3"
return "0"
mock.git.rev_list.side_effect = rev_list_side_effect
with (
patch.object(repo, "_repo_or_raise", return_value=mock),
patch.object(
repo,
"_get_remote_default_branch",
AsyncMock(return_value="origin/main"),
),
):
assert await repo.get_unpushed_commit_count() == 3
class TestGitRepositoryPushCurrentBranch:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_returns_false_when_no_branch(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], is_detached=True)
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.push_current_branch() is False
@pytest.mark.asyncio
async def test_returns_false_when_push_fails(self, repo: GitRepository) -> None:
from git.exc import GitCommandError
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], branch="main")
mock.remote.return_value.push.side_effect = GitCommandError("git push", 1)
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.push_current_branch() is False
@pytest.mark.asyncio
async def test_returns_true_when_push_succeeds(self, repo: GitRepository) -> None:
mock = make_mock_repo(urls=["git@github.com:owner/repo.git"], branch="main")
with patch.object(repo, "_repo_or_raise", return_value=mock):
assert await repo.push_current_branch() is True
class TestGitRepositoryGetRemoteDefaultBranch:
@pytest.fixture
def repo(self, tmp_path: Path) -> GitRepository:
return GitRepository(tmp_path)
@pytest.mark.asyncio
async def test_returns_default_branch_when_head_exists(
self, repo: GitRepository
) -> None:
mock = MagicMock()
mock_head_ref = MagicMock()
mock_head_ref.reference.name = "origin/main"
mock.remotes.__getitem__.return_value.refs.HEAD = mock_head_ref
mock.git.rev_parse.return_value = "abc123"
result = await repo._get_remote_default_branch(mock, "origin")
assert result == "origin/main"
@pytest.mark.asyncio
async def test_returns_none_when_remote_not_found(
self, repo: GitRepository
) -> None:
mock = MagicMock()
mock.remotes.__getitem__.side_effect = IndexError("No item found")
result = await repo._get_remote_default_branch(mock, "upstream")
assert result is None
@pytest.mark.asyncio
async def test_returns_none_when_head_ref_missing(
self, repo: GitRepository
) -> None:
mock = MagicMock()
mock.remotes.__getitem__.return_value.refs.HEAD = None
result = await repo._get_remote_default_branch(mock, "origin")
assert result is None