from pathlib import Path from types import SimpleNamespace from agentci.domain.models import ( AgentResult, Job, JobKind, ReviewFinding, ReviewReport, ReviewSeverity, Workflow, WorkflowKind, ) from agentci.workflows.code_review import CodeReviewLoop def serious_report() -> ReviewReport: return ReviewReport( summary="Needs work", findings=[ ReviewFinding( severity=ReviewSeverity.MAJOR, title="Missing check", detail="A check is absent.", recommendation="Add it.", ) ], ) class FakeOpenCode: def __init__(self, reports: list[ReviewReport]) -> None: self.reports = iter(reports) self.reviews = 0 self.revisions = 0 async def create_session(self, *_args): return "reviewer" async def resume(self, **kwargs): if kwargs["result_type"] is ReviewReport: self.reviews += 1 return next(self.reports) self.revisions += 1 return AgentResult(summary_markdown=f"revision {self.revisions}", tests=[]) class FakeStorage: async def update_job(self, *_args, **_kwargs): return None async def update_workflow(self, *_args, **_kwargs): return None class FakePrompts: def render(self, name, **_kwargs): return name def objects(rounds: int, reports: list[ReviewReport]): opencode = FakeOpenCode(reports) settings = SimpleNamespace( implement_review_rounds=rounds, implement_model="model", implement_variant="high", ) deps = SimpleNamespace( settings=settings, opencode=opencode, storage=FakeStorage(), prompts=FakePrompts(), development=SimpleNamespace(description="python"), ) workflow = Workflow( id="flow", kind=WorkflowKind.IMPLEMENT, repo_owner="org", repo_name="repo", issue_number=1, workspace_path=Path("."), base_sha="abc", primary_session_id="primary", ) job = Job( id="job", kind=JobKind.IMPLEMENT, target_key="target", repo_owner="org", repo_name="repo", issue_number=1, pr_number=None, requester="alice", message="", comment_id=1, ) return CodeReviewLoop(deps), opencode, workflow, job # type: ignore[arg-type] async def test_stops_after_clean_second_review() -> None: clean = ReviewReport(summary="Ready", findings=[]) loop, opencode, workflow, job = objects(4, [serious_report(), clean]) _, report = await loop.run( job, workflow, "issue context", "canonical plan", AgentResult(summary_markdown="initial", tests=[]), ) assert not report.has_serious_findings assert opencode.reviews == 2 assert opencode.revisions == 1 async def test_does_not_make_unreviewed_final_revision() -> None: loop, opencode, workflow, job = objects( 3, [serious_report(), serious_report(), serious_report()] ) _, report = await loop.run( job, workflow, "issue context", "canonical plan", AgentResult(summary_markdown="initial", tests=[]), ) assert report.has_serious_findings assert opencode.reviews == 3 assert opencode.revisions == 2