""" 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")