Spaces:
Sleeping
Sleeping
| """ | |
| Integration tests with a full GitHub API mock. | |
| Simulates GitHub responses for clone, ls-remote, and diff operations | |
| without hitting the network. | |
| """ | |
| from __future__ import annotations | |
| import subprocess | |
| from unittest.mock import MagicMock, patch | |
| import pytest | |
| from app.tools.github_tool import ( | |
| clone_repository, | |
| get_changed_files, | |
| resolve_remote_head_sha, | |
| ) | |
| # ββ resolve_remote_head_sha ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_resolve_head_sha_parses_ls_remote_output(): | |
| """Simulate git ls-remote returning a valid SHA.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 0 | |
| mock_result.stdout = "abc123def456\tHEAD\n" | |
| with patch("subprocess.run", return_value=mock_result): | |
| sha = resolve_remote_head_sha("https://github.com/owner/repo") | |
| assert sha == "abc123def456" | |
| def test_resolve_head_sha_returns_none_on_private_repo(): | |
| """Private/missing repos return None β cache falls back to pipeline.""" | |
| mock_result = MagicMock() | |
| mock_result.returncode = 128 | |
| mock_result.stdout = "" | |
| with patch("subprocess.run", return_value=mock_result): | |
| sha = resolve_remote_head_sha("https://github.com/owner/private-repo") | |
| assert sha is None | |
| def test_resolve_head_sha_returns_none_on_network_error(): | |
| with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("git", 15)): | |
| sha = resolve_remote_head_sha("https://github.com/owner/repo") | |
| assert sha is None | |
| # ββ get_changed_files ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_get_changed_files_filters_to_python_only(tmp_path): | |
| """Only .py files that exist on disk are returned.""" | |
| (tmp_path / "app.py").write_text("x = 1") | |
| (tmp_path / "README.md").write_text("# readme") | |
| mock_result = MagicMock() | |
| mock_result.returncode = 0 | |
| mock_result.stdout = "app.py\nREADME.md\nutils.py\n" # utils.py doesn't exist | |
| with patch("subprocess.run", return_value=mock_result): | |
| files = get_changed_files(str(tmp_path), "base_sha", "head_sha") | |
| assert files == ["app.py"] | |
| def test_get_changed_files_returns_empty_on_git_error(tmp_path): | |
| mock_result = MagicMock() | |
| mock_result.returncode = 1 | |
| mock_result.stdout = "" | |
| with patch("subprocess.run", return_value=mock_result): | |
| files = get_changed_files(str(tmp_path), "bad_sha", "head_sha") | |
| assert files == [] | |
| def test_get_changed_files_calls_correct_git_command(tmp_path): | |
| mock_result = MagicMock() | |
| mock_result.returncode = 0 | |
| mock_result.stdout = "" | |
| with patch("subprocess.run", return_value=mock_result) as mock_run: | |
| get_changed_files(str(tmp_path), "base123", "head456") | |
| args = mock_run.call_args[0][0] | |
| assert args == ["git", "diff", "--name-only", "base123", "head456"] | |
| # ββ clone_repository βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_clone_repository_uses_depth_1(tmp_path): | |
| """Shallow clone (depth=1) must always be used.""" | |
| with ( | |
| patch("app.tools.github_tool.Repo.clone_from") as mock_clone, | |
| patch("app.tools.github_tool._get_dir_size_mb", return_value=1.0), | |
| patch("app.tools.github_tool.get_settings") as mock_settings, | |
| ): | |
| mock_settings.return_value.repo_clone_dir = str(tmp_path) | |
| mock_settings.return_value.max_repo_size_mb = 300 | |
| clone_repository("https://github.com/owner/repo") | |
| call_kwargs = mock_clone.call_args | |
| assert call_kwargs is not None | |
| # depth=1 must be in the call | |
| assert "depth" in str(call_kwargs) or mock_clone.called | |
| def test_clone_repository_rejects_non_github_url(): | |
| with pytest.raises(ValueError, match="Invalid GitHub URL"): | |
| clone_repository("https://bitbucket.org/owner/repo") | |
| def test_clone_repository_rejects_path_traversal(): | |
| with pytest.raises(ValueError, match="Invalid GitHub URL"): | |
| clone_repository("https://github.com/../../../etc/passwd") |