# tests/test_code_review_agent_extended.py from unittest.mock import AsyncMock, patch import pytest from app.agents import code_review_agent from app.models.repository import RepositoryMetadata def _metadata( base_sha: str | None = None, head_sha: str | None = None ) -> RepositoryMetadata: return RepositoryMetadata( url="https://github.com/test/repo", name="repo", local_path="/tmp/fake", base_sha=base_sha, head_sha=head_sha, ) @pytest.mark.asyncio async def test_code_review_uses_changed_files_when_shas_provided(tmp_path) -> None: """When base_sha and head_sha are set, get_changed_files is called.""" src = tmp_path / "main.py" src.write_text("def foo(): pass\n") with ( patch( "app.agents.code_review_agent.get_changed_files", return_value=[str(src)], ) as mock_diff, patch( "app.agents.code_review_agent.call_llm_for_json", new=AsyncMock( return_value={ "solid_violations": [], "duplicate_code": [], "refactor_suggestions": [], "overall_score": 7.0, "summary": "ok", } ), ), ): result = await code_review_agent.run( str(tmp_path), _metadata(base_sha="abc", head_sha="def"), ) mock_diff.assert_called_once() assert result.overall_score == 7.0 @pytest.mark.asyncio async def test_code_review_skips_test_files(tmp_path) -> None: """Files named test_*.py are excluded from the review.""" test_file = tmp_path / "test_something.py" test_file.write_text("def test_x(): pass\n") with patch( "app.agents.code_review_agent.call_llm_for_json", new=AsyncMock(return_value={}), ): result = await code_review_agent.run(str(tmp_path), _metadata()) # Only test files present → no source samples → score 0 assert result.overall_score == 0.0 @pytest.mark.asyncio async def test_code_review_low_score_triggers_followup(tmp_path) -> None: """A score below 5.0 triggers a second LLM call for priority fixes.""" src = tmp_path / "main.py" src.write_text("def foo(): pass\n") responses = iter( [ # first call: main review — score is 3.0 (triggers second pass) { "solid_violations": ["SRP violation"], "duplicate_code": [], "refactor_suggestions": [], "overall_score": 3.0, "summary": "poor code", }, # auto-fix calls (top_findings[:3], but solid_violations has 1 item) { "file": "", "original_snippet": "", "fixed_snippet": "", "explanation": "n/a", }, # second pass: priority fixes {"priority_fixes": ["fix SRP first"]}, ] ) with patch( "app.agents.code_review_agent.call_llm_for_json", new=AsyncMock(side_effect=lambda *a, **kw: next(responses)), ): result = await code_review_agent.run(str(tmp_path), _metadata()) assert "fix SRP first" in result.refactor_suggestions @pytest.mark.asyncio async def test_code_review_handles_file_read_error(tmp_path) -> None: """A file that raises on open is skipped gracefully.""" src = tmp_path / "main.py" src.write_text("def foo(): pass\n") original_open = open def patched_open(path, *args, **kwargs): # type: ignore[no-untyped-def] if str(path).endswith("main.py"): raise OSError("permission denied") return original_open(path, *args, **kwargs) with ( patch("builtins.open", side_effect=patched_open), patch( "app.agents.code_review_agent.call_llm_for_json", new=AsyncMock(return_value={}), ), ): result = await code_review_agent.run(str(tmp_path), _metadata()) # No readable files → falls back to empty review assert result.overall_score == 0.0