File size: 4,134 Bytes
229af03
a79668c
229af03
 
 
 
 
 
 
a79668c
 
 
229af03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a79668c
 
 
 
 
 
 
 
 
229af03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a79668c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229af03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a79668c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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