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 FakeCodex: def __init__(self, reports: list[ReviewReport]) -> None: self.reports = iter(reports) self.reviews = 0 self.revisions = 0 async def start(self, **_kwargs): self.reviews += 1 return "reviewer", next(self.reports) 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]): codex = FakeCodex(reports) settings = SimpleNamespace( implement_review_rounds=rounds, implement_model="model", implement_reasoning="high", ) deps = SimpleNamespace( settings=settings, codex=codex, storage=FakeStorage(), prompts=FakePrompts(), ) 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), codex, workflow, job # type: ignore[arg-type] async def test_stops_after_clean_second_review() -> None: clean = ReviewReport(summary="Ready", findings=[]) loop, codex, 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 codex.reviews == 2 assert codex.revisions == 1 async def test_does_not_make_unreviewed_final_revision() -> None: loop, codex, 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 codex.reviews == 3 assert codex.revisions == 2