Files
roboco/tests/unit/gateway/test_pr_review_verdict_consistency.py
T

105 lines
2.8 KiB
Python
Raw Normal View History

"""post_pr_review refuses a self-contradicting verdict before it is recorded/posted.
The recorded ``notes_structured.pr_review.verdict`` and the GitHub review event
both derive from the verb's ``event`` argument. A reviewer who concludes
"approve" in the body but leaves ``event`` at its ``REQUEST_CHANGES`` default
(with no findings) would otherwise file — and post to the contributor's PR — a
blocking "changes requested" that contradicts the approving summary. The gate
catches that at the choreographer before any side effect runs.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
def _make_choreographer() -> Choreographer:
base: dict[str, Any] = {
"task": AsyncMock(),
"work_session": AsyncMock(),
"git": AsyncMock(),
"a2a": AsyncMock(),
"journal": AsyncMock(),
"audit": AsyncMock(),
"evidence_repo": AsyncMock(),
}
return Choreographer(ChoreographerDeps(**base))
_CLEAN_FINDING = {
"file": "a.py",
"severity": "minor",
"expected": "x",
"actual": "y",
}
_BLOCKER = {"file": "a.py", "severity": "blocker", "expected": "x", "actual": "y"}
@pytest.mark.asyncio
async def test_request_changes_without_findings_is_rejected() -> None:
c = _make_choreographer()
env = await c._verdict_consistency_gate(
MagicMock(),
uuid4(),
uuid4(),
"pr_reviewer",
{},
event="REQUEST_CHANGES",
findings=[],
)
assert env is not None
body = env.as_dict()
assert body["error"] == "invalid_state"
assert "finding" in body["message"].lower()
@pytest.mark.asyncio
async def test_approve_over_a_blocker_is_rejected() -> None:
c = _make_choreographer()
env = await c._verdict_consistency_gate(
MagicMock(),
uuid4(),
uuid4(),
"pr_reviewer",
{},
event="APPROVE",
findings=[_BLOCKER],
)
assert env is not None
assert env.as_dict()["error"] == "invalid_state"
@pytest.mark.asyncio
async def test_request_changes_with_findings_passes_gate() -> None:
c = _make_choreographer()
env = await c._verdict_consistency_gate(
MagicMock(),
uuid4(),
uuid4(),
"pr_reviewer",
{},
event="REQUEST_CHANGES",
findings=[_CLEAN_FINDING],
)
assert env is None
@pytest.mark.asyncio
async def test_clean_approve_passes_gate() -> None:
c = _make_choreographer()
env = await c._verdict_consistency_gate(
MagicMock(),
uuid4(),
uuid4(),
"pr_reviewer",
{},
event="APPROVE",
findings=[],
)
assert env is None