ai-code-review-agent / tests /test_github_tool.py
Padmanav's picture
fix
af5af97
Raw
History Blame Contribute Delete
7.03 kB
# tests/test_github_tool.py
import concurrent.futures
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from app.tools.github_tool import (
_get_dir_size_mb,
clone_repository,
delete_repository,
get_changed_files,
resolve_remote_head_sha,
)
# ── _get_dir_size_mb ──────────────────────────────────────────────────────────
def test_get_dir_size_mb_sums_file_sizes(tmp_path: Path) -> None:
(tmp_path / "a.txt").write_bytes(b"x" * 1024)
(tmp_path / "b.txt").write_bytes(b"x" * 1024)
size = _get_dir_size_mb(tmp_path)
assert size == pytest.approx(2 / 1024, rel=0.01)
# ── get_changed_files ─────────────────────────────────────────────────────────
def test_get_changed_files_returns_py_files(tmp_path: Path) -> None:
(tmp_path / "changed.py").write_text("x = 1")
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "changed.py\nREADME.md\n"
with patch("app.tools.github_tool.subprocess.run", return_value=mock_result):
files = get_changed_files(str(tmp_path), "abc123", "def456")
assert files == [str(tmp_path / "changed.py") if False else "changed.py"]
# only .py files that exist on disk are returned
assert all(f.endswith(".py") for f in files)
def test_get_changed_files_returns_empty_on_git_error() -> None:
mock_result = MagicMock()
mock_result.returncode = 1
with patch("app.tools.github_tool.subprocess.run", return_value=mock_result):
files = get_changed_files("/fake/path", "abc", "def")
assert files == []
def test_get_changed_files_filters_nonexistent_files(tmp_path: Path) -> None:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "ghost.py\n" # file does NOT exist on disk
with patch("app.tools.github_tool.subprocess.run", return_value=mock_result):
files = get_changed_files(str(tmp_path), "abc", "def")
assert files == []
# ── clone_repository ──────────────────────────────────────────────────────────
def test_clone_repository_rejects_invalid_url() -> None:
with pytest.raises(ValueError, match="Invalid GitHub URL"):
clone_repository("https://gitlab.com/owner/repo")
def test_clone_repository_strips_dot_git_suffix(tmp_path: Path) -> None:
with (
patch("app.tools.github_tool.settings") as mock_settings,
patch("app.tools.github_tool.Repo.clone_from"),
patch("app.tools.github_tool._get_dir_size_mb", return_value=1.0),
):
mock_settings.repo_clone_dir = str(tmp_path)
mock_settings.max_repo_size_mb = 300
result = clone_repository("https://github.com/owner/myrepo.git")
assert "myrepo" in result
assert ".git" not in Path(result).name
def test_clone_repository_success(tmp_path: Path) -> None:
with (
patch("app.tools.github_tool.settings") as mock_settings,
patch("app.tools.github_tool.Repo.clone_from"),
patch("app.tools.github_tool._get_dir_size_mb", return_value=10.0),
):
mock_settings.repo_clone_dir = str(tmp_path)
mock_settings.max_repo_size_mb = 300
result = clone_repository("https://github.com/owner/repo")
assert "repo" in result
def test_clone_repository_raises_on_timeout(tmp_path: Path) -> None:
def slow_clone(*args: object, **kwargs: object) -> None:
raise concurrent.futures.TimeoutError()
with (
patch("app.tools.github_tool.settings") as mock_settings,
patch("app.tools.github_tool.Repo.clone_from", side_effect=slow_clone),
patch("app.tools.github_tool._get_dir_size_mb", return_value=1.0),
):
mock_settings.repo_clone_dir = str(tmp_path)
mock_settings.max_repo_size_mb = 300
with pytest.raises(TimeoutError, match="timed out"):
clone_repository("https://github.com/owner/repo")
def test_clone_repository_raises_on_clone_error(tmp_path: Path) -> None:
with (
patch("app.tools.github_tool.settings") as mock_settings,
patch(
"app.tools.github_tool.Repo.clone_from",
side_effect=RuntimeError("auth failed"),
),
):
mock_settings.repo_clone_dir = str(tmp_path)
mock_settings.max_repo_size_mb = 300
with pytest.raises(RuntimeError, match="auth failed"):
clone_repository("https://github.com/owner/repo")
def test_clone_repository_raises_when_too_large(tmp_path: Path) -> None:
with (
patch("app.tools.github_tool.settings") as mock_settings,
patch("app.tools.github_tool.Repo.clone_from"),
patch("app.tools.github_tool._get_dir_size_mb", return_value=999.0),
):
mock_settings.repo_clone_dir = str(tmp_path)
mock_settings.max_repo_size_mb = 300
with pytest.raises(ValueError, match="exceeds limit"):
clone_repository("https://github.com/owner/repo")
# ── delete_repository ─────────────────────────────────────────────────────────
def test_delete_repository_removes_existing_path(tmp_path: Path) -> None:
target = tmp_path / "cloned_repo"
target.mkdir()
(target / "file.py").write_text("x = 1")
delete_repository(str(target))
assert not target.exists()
def test_delete_repository_is_noop_for_missing_path(tmp_path: Path) -> None:
missing = str(tmp_path / "does_not_exist")
delete_repository(missing) # must not raise
# ── resolve_remote_head_sha ─────────────────────────────────────────────────
def test_resolve_remote_head_sha_returns_sha_on_success() -> None:
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "abc123def456\tHEAD\n"
with patch("app.tools.github_tool.subprocess.run", return_value=mock_result):
sha = resolve_remote_head_sha("https://github.com/owner/repo")
assert sha == "abc123def456"
def test_resolve_remote_head_sha_returns_none_on_git_error() -> None:
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
with patch("app.tools.github_tool.subprocess.run", return_value=mock_result):
sha = resolve_remote_head_sha("https://github.com/owner/repo")
assert sha is None
def test_resolve_remote_head_sha_returns_none_on_exception() -> None:
with patch("app.tools.github_tool.subprocess.run", side_effect=OSError("boom")):
sha = resolve_remote_head_sha("https://github.com/owner/repo")
assert sha is None