130 lines
3.3 KiB
Python
130 lines
3.3 KiB
Python
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(),
|
|
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), 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
|