vibe/tests/core/test_teleport_git.py
maiengineering ac8f1a09fd
v2.18.4 (#866)
Co-authored-by: Albert Jiang <aj@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Laure Hugo <201583486+laure0303@users.noreply.github.com>
Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai>
Co-authored-by: Mert Unsal <mert.unsal@mistral.ai>
Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com>
Co-authored-by: Paul VEZIA <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-07-01 19:03:09 +02:00

377 lines
14 KiB
Python

from __future__ import annotations
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from git import Repo
import pytest
from vibe.core.teleport.errors import (
ServiceTeleportError,
ServiceTeleportNotSupportedError,
)
from vibe.core.teleport.git import GitRepoInfo, GitRepository
def make_mock_remote(url: str, name: str = "origin") -> MagicMock:
remote = MagicMock()
remote.name = name
remote.urls = [url]
return remote
def make_mock_repo(
urls: list[str] | None = None,
remote_names: list[str] | None = None,
commit: str | None = "abc123",
branch: str | None = "main",
is_detached: bool = False,
diff: str = "",
) -> MagicMock:
mock = MagicMock()
if urls:
names = remote_names or [
"origin" if index == 0 else f"remote-{index}"
for index, _ in enumerate(urls)
]
mock.remotes = [
make_mock_remote(url, names[index]) for index, url in enumerate(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.index.path = str(Path("/tmp/mock-index"))
mock.remote.return_value = make_mock_remote(urls[0] if urls else "")
return mock
def make_real_repo(workdir: Path) -> Repo:
repo = Repo.init(workdir, initial_branch="main")
repo.config_writer().set_value("user", "name", "Tester").release()
repo.config_writer().set_value("user", "email", "t@example.com").release()
(workdir / "tracked.txt").write_text("base\n")
repo.index.add(["tracked.txt"])
repo.index.commit("initial")
return repo
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="Teleport requires 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_name="origin",
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_returns_matched_github_remote_name(
self, repo: GitRepository
) -> None:
mock = make_mock_repo(
urls=["git@gitlab.com:owner/repo.git", "git@github.com:owner/repo.git"],
remote_names=["origin", "hub"],
)
with patch.object(repo, "_repo_or_raise", return_value=mock):
info = await repo.get_info()
assert info.remote_name == "hub"
assert info.remote_url == "https://github.com/owner/repo.git"
@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 TestGitRepositoryGetDiff:
@pytest.mark.asyncio
async def test_includes_untracked_files_without_mutating_index(
self, tmp_path: Path
) -> None:
source_repo = make_real_repo(tmp_path)
(tmp_path / "new.txt").write_text("hello\n")
status_before = source_repo.git.status("--short")
async with GitRepository(tmp_path) as git_repo:
diff = await git_repo._get_diff(source_repo)
assert "diff --git a/new.txt b/new.txt" in diff
assert source_repo.git.status("--short") == status_before
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