from __future__ import annotations from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest from agent.reviewer_groups import ( _build_prompt, _DiffGroupingResult, _DiffGroupModel, diff_signature, generate_diff_groups, maybe_generate_and_store_diff_groups, ) _DIFF = """diff --git a/foo.py b/foo.py --- a/foo.py +++ b/foo.py @@ -1,2 +1,3 @@ a +b c diff --git a/bar.py b/bar.py --- a/bar.py +++ b/bar.py @@ -1 +1,2 @@ x +y """ class _FakeStructured: def __init__(self, result: Any, *, raises: bool = False) -> None: self._result = result self._raises = raises async def ainvoke(self, _prompt: str) -> Any: if self._raises: raise RuntimeError("boom") return self._result class _FakeModel: def __init__(self, result: Any, *, raises: bool = False) -> None: self._structured = _FakeStructured(result, raises=raises) def with_structured_output(self, _schema: Any) -> _FakeStructured: return self._structured @pytest.mark.asyncio async def test_generate_diff_groups_partitions() -> None: result = _DiffGroupingResult( groups=[ _DiffGroupModel(title="Foo change", summary="Edits foo", files=["foo.py"]), _DiffGroupModel(title="Bar change", summary="Edits bar", files=["bar.py"]), ] ) groups = await generate_diff_groups(diff_text=_DIFF, model=_FakeModel(result)) assert groups == [ {"title": "Foo change", "summary": "Edits foo", "files": ["foo.py"]}, {"title": "Bar change", "summary": "Edits bar", "files": ["bar.py"]}, ] @pytest.mark.asyncio async def test_generate_diff_groups_dedupes_and_drops_unknown() -> None: result = _DiffGroupingResult( groups=[ _DiffGroupModel( title="First", summary="", files=["foo.py", "foo.py", "does-not-exist.py"], ), _DiffGroupModel(title="Second", summary="", files=["foo.py", "bar.py"]), _DiffGroupModel(title="", summary="ignored", files=["bar.py"]), ] ) groups = await generate_diff_groups(diff_text=_DIFF, model=_FakeModel(result)) # foo.py only in the first group, bar.py only in the second; the untitled # group and the unknown path are dropped. assert groups == [ {"title": "First", "summary": "", "files": ["foo.py"]}, {"title": "Second", "summary": "", "files": ["bar.py"]}, ] @pytest.mark.asyncio async def test_generate_diff_groups_empty_diff_returns_empty() -> None: assert await generate_diff_groups(diff_text="", model=_FakeModel(None)) == [] @pytest.mark.asyncio async def test_generate_diff_groups_llm_failure_returns_none() -> None: groups = await generate_diff_groups(diff_text=_DIFF, model=_FakeModel(None, raises=True)) assert groups is None def test_build_prompt_includes_files_and_diffs() -> None: prompt = _build_prompt(_DIFF, ["foo.py", "bar.py"]) assert "### foo.py" in prompt assert "### bar.py" in prompt # Hunks are rendered as plain diff fences (no line-range annotations). assert "lines 1-3:" not in prompt assert "```diff" in prompt # The verbatim file list is included so every file can be assigned. assert "- foo.py" in prompt assert "- bar.py" in prompt def test_diff_signature_is_stable_and_content_sensitive() -> None: assert diff_signature(_DIFF) == diff_signature(_DIFF) assert diff_signature(_DIFF) != diff_signature(_DIFF + "\n+extra") @pytest.mark.asyncio async def test_maybe_generate_skips_when_signature_unchanged() -> None: existing = { "diff_groups": { "signature": diff_signature(_DIFF), "groups": [{"title": "t", "summary": "", "files": ["foo.py"]}], } } with ( patch( "agent.reviewer_groups.get_thread_metadata", new_callable=AsyncMock, return_value=existing, ), patch("agent.reviewer_groups.generate_diff_groups", new_callable=AsyncMock) as gen, patch( "agent.reviewer_groups.set_reviewer_thread_metadata", new_callable=AsyncMock, ) as set_meta, ): await maybe_generate_and_store_diff_groups( thread_id="t1", head_sha="h", diff_text=_DIFF, model=MagicMock() ) gen.assert_not_awaited() set_meta.assert_not_awaited() @pytest.mark.asyncio async def test_maybe_generate_stores_when_changed() -> None: groups = [{"title": "t", "summary": "s", "files": ["foo.py"]}] with ( patch( "agent.reviewer_groups.get_thread_metadata", new_callable=AsyncMock, return_value={}, ), patch( "agent.reviewer_groups.generate_diff_groups", new_callable=AsyncMock, return_value=groups, ) as gen, patch( "agent.reviewer_groups.set_reviewer_thread_metadata", new_callable=AsyncMock, ) as set_meta, ): await maybe_generate_and_store_diff_groups( thread_id="t1", head_sha="head123", diff_text=_DIFF, model=MagicMock() ) gen.assert_awaited_once() set_meta.assert_awaited_once() payload = set_meta.call_args.kwargs["extra"]["diff_groups"] assert payload["head_sha"] == "head123" assert payload["signature"] == diff_signature(_DIFF) assert payload["groups"] == groups