This commit is contained in:
@@ -1,33 +1,15 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from agentci.engine.model import Job, JobKind, Workflow, WorkflowKind, WorkflowStatus
|
||||
from agentci.engine.run import JobRun
|
||||
from agentci.integrations.gitea.models import CommentInfo, IssueInfo, PullRequestInfo
|
||||
from agentci.workflows.implementation import implement
|
||||
from agentci.workflows.model import AgentResult, ReviewReport
|
||||
from agentci.workflows.render import JobRejected
|
||||
from agentci.workflows.services import WorkflowServices
|
||||
|
||||
|
||||
class RecordingRun(JobRun):
|
||||
def __init__(self) -> None:
|
||||
self.stages: list[str] = []
|
||||
self.created_workflows: list[tuple[Workflow, str]] = []
|
||||
self.linked_sessions: list[str] = []
|
||||
|
||||
async def stage(self, stage: str) -> None:
|
||||
self.stages.append(stage)
|
||||
|
||||
async def create_workflow(self, workflow: Workflow, stage: str) -> None:
|
||||
self.created_workflows.append((workflow, stage))
|
||||
|
||||
async def link_session(self, session_id: str) -> None:
|
||||
self.linked_sessions.append(session_id)
|
||||
from tests.workflow_support import make_workflow_harness
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
@@ -85,71 +67,6 @@ class FakeGitea:
|
||||
return pull(number=42, branch=values["head"])
|
||||
|
||||
|
||||
class RecordingGit:
|
||||
def __init__(self, *, changed: bool = True) -> None:
|
||||
self.changed = changed
|
||||
self.calls: list[tuple[Any, ...]] = []
|
||||
|
||||
async def clone(self, owner: str, repo: str, branch: str, destination: Path) -> str:
|
||||
self.calls.append(("clone", owner, repo, branch, destination))
|
||||
return "base-sha"
|
||||
|
||||
async def create_branch(self, workspace: Path, branch: str) -> None:
|
||||
self.calls.append(("create_branch", workspace, branch))
|
||||
|
||||
async def has_changes(self, workspace: Path) -> bool:
|
||||
self.calls.append(("has_changes", workspace))
|
||||
return self.changed
|
||||
|
||||
async def diff_check(self, workspace: Path) -> None:
|
||||
self.calls.append(("diff_check", workspace))
|
||||
|
||||
async def commit(self, workspace: Path, message: str) -> str:
|
||||
self.calls.append(("commit", workspace, message))
|
||||
return "commit-sha"
|
||||
|
||||
async def push(self, workspace: Path, branch: str, *, set_upstream: bool = False) -> None:
|
||||
self.calls.append(("push", workspace, branch, set_upstream))
|
||||
|
||||
|
||||
class RecordingDevelopment:
|
||||
description = "Python 3.13"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.workspaces: list[Path] = []
|
||||
|
||||
async def prepare(self, workspace: Path) -> None:
|
||||
self.workspaces.append(workspace)
|
||||
|
||||
|
||||
class RecordingPrompts:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[tuple[str, dict[str, str]]] = []
|
||||
|
||||
def render(self, name: str, **values: str) -> str:
|
||||
self.calls.append((name, values))
|
||||
return f"rendered {name}"
|
||||
|
||||
|
||||
class RecordingOpenCode:
|
||||
def __init__(self, result: AgentResult, report: ReviewReport) -> None:
|
||||
self.result = result
|
||||
self.report = report
|
||||
self.created_sessions: list[tuple[Path, str]] = []
|
||||
self.resume_calls: list[dict[str, Any]] = []
|
||||
|
||||
async def create_session(self, workspace: Path, title: str) -> str:
|
||||
self.created_sessions.append((workspace, title))
|
||||
return f"{title}-session"
|
||||
|
||||
async def resume(self, **values: Any) -> AgentResult | ReviewReport:
|
||||
self.resume_calls.append(values)
|
||||
if values["result_type"] is AgentResult:
|
||||
return self.result
|
||||
assert values["result_type"] is ReviewReport
|
||||
return self.report
|
||||
|
||||
|
||||
def job() -> Job:
|
||||
return Job(
|
||||
id="job-1",
|
||||
@@ -203,95 +120,63 @@ def pull(
|
||||
)
|
||||
|
||||
|
||||
def make_services(
|
||||
tmp_path: Path,
|
||||
*,
|
||||
changed: bool = True,
|
||||
implementations: list[Workflow] | None = None,
|
||||
existing_pulls: dict[int, PullRequestInfo] | None = None,
|
||||
plan: Workflow | None = None,
|
||||
) -> tuple[
|
||||
WorkflowServices,
|
||||
FakeRepository,
|
||||
FakeGitea,
|
||||
RecordingGit,
|
||||
RecordingDevelopment,
|
||||
RecordingPrompts,
|
||||
RecordingOpenCode,
|
||||
]:
|
||||
repository = FakeRepository(implementations=implementations, plan=plan)
|
||||
gitea = FakeGitea(existing_pulls=existing_pulls)
|
||||
git = RecordingGit(changed=changed)
|
||||
development = RecordingDevelopment()
|
||||
prompts = RecordingPrompts()
|
||||
opencode = RecordingOpenCode(
|
||||
AgentResult(
|
||||
summary_markdown="# Implement widget\n\nHandled empty input.",
|
||||
tests=["pytest: passed"],
|
||||
),
|
||||
ReviewReport(summary="Ready", findings=[]),
|
||||
def implementation_settings(tmp_path: Path) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
branch_prefix="agent",
|
||||
workspaces_dir=tmp_path / "workspaces",
|
||||
gitea_url="https://git.example.test",
|
||||
implement_model="provider/model",
|
||||
implement_variant="high",
|
||||
implement_review_rounds=2,
|
||||
)
|
||||
services = cast(
|
||||
WorkflowServices,
|
||||
SimpleNamespace(
|
||||
settings=SimpleNamespace(
|
||||
branch_prefix="agent",
|
||||
workspaces_dir=tmp_path / "workspaces",
|
||||
gitea_url="https://git.example.test",
|
||||
implement_model="provider/model",
|
||||
implement_variant="high",
|
||||
implement_review_rounds=2,
|
||||
),
|
||||
repository=repository,
|
||||
gitea=gitea,
|
||||
git=git,
|
||||
opencode=opencode,
|
||||
prompts=prompts,
|
||||
development=development,
|
||||
),
|
||||
)
|
||||
return services, repository, gitea, git, development, prompts, opencode
|
||||
|
||||
|
||||
async def test_initial_implementation_completes_review_commit_push_and_pr(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
stale = workflow(pr_number=8)
|
||||
services, repository, gitea, git, development, prompts, opencode = make_services(
|
||||
tmp_path,
|
||||
implementations=[stale],
|
||||
existing_pulls={8: pull(state="closed")},
|
||||
repository = FakeRepository(implementations=[stale])
|
||||
gitea = FakeGitea(existing_pulls={8: pull(state="closed")})
|
||||
harness = make_workflow_harness(
|
||||
settings=implementation_settings(tmp_path),
|
||||
repository=repository,
|
||||
gitea=gitea,
|
||||
responses=[
|
||||
AgentResult(
|
||||
summary_markdown="# Implement widget\n\nHandled empty input.",
|
||||
tests=["pytest: passed"],
|
||||
),
|
||||
ReviewReport(summary="Ready", findings=[]),
|
||||
],
|
||||
)
|
||||
run = RecordingRun()
|
||||
|
||||
body = await implement(job(), run, services)
|
||||
body = await implement(job(), harness.run, harness.services)
|
||||
|
||||
assert len(run.created_workflows) == 1
|
||||
created, created_stage = run.created_workflows[0]
|
||||
assert len(harness.run.created_workflows) == 1
|
||||
created, created_stage = harness.run.created_workflows[0]
|
||||
assert created_stage == "installing development environment"
|
||||
assert created.status is WorkflowStatus.ACTIVE
|
||||
assert created.branch == f"agent/issue-7-{created.id[:8]}"
|
||||
assert created.workspace_path == tmp_path / "workspaces" / created.id / "repo"
|
||||
assert created.base_sha == "base-sha"
|
||||
assert development.workspaces == [created.workspace_path]
|
||||
assert run.linked_sessions == ["implementation-session"]
|
||||
assert opencode.created_sessions == [
|
||||
assert harness.development.workspaces == [created.workspace_path]
|
||||
assert harness.run.linked_sessions == ["implementation-session"]
|
||||
assert harness.opencode.created_sessions == [
|
||||
(created.workspace_path, "implementation"),
|
||||
(created.workspace_path, "implementation-review"),
|
||||
]
|
||||
assert [call["result_type"] for call in opencode.resume_calls] == [
|
||||
assert [call["result_type"] for call in harness.opencode.resume_calls] == [
|
||||
AgentResult,
|
||||
ReviewReport,
|
||||
]
|
||||
|
||||
initial_prompt = prompts.calls[0]
|
||||
initial_prompt = harness.prompts.calls[0]
|
||||
assert initial_prompt[0] == "implement_initial"
|
||||
assert initial_prompt[1]["artifact"] == "(no canonical plan)"
|
||||
assert initial_prompt[1]["request"] == "Keep the change focused."
|
||||
assert "Please cover empty input." in initial_prompt[1]["context"]
|
||||
assert "Job queued" not in initial_prompt[1]["context"]
|
||||
assert initial_prompt[1]["context"].startswith("Repository: org/repo\n")
|
||||
|
||||
assert git.calls == [
|
||||
assert harness.git.calls == [
|
||||
("clone", "org", "repo", "main", created.workspace_path),
|
||||
("create_branch", created.workspace_path, created.branch),
|
||||
("has_changes", created.workspace_path),
|
||||
@@ -338,16 +223,29 @@ async def test_initial_implementation_completes_review_commit_push_and_pr(
|
||||
async def test_initial_implementation_rejects_clean_worktree_before_commit_or_pr(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
services, repository, gitea, git, _, _, _ = make_services(tmp_path, changed=False)
|
||||
run = RecordingRun()
|
||||
repository = FakeRepository()
|
||||
gitea = FakeGitea()
|
||||
harness = make_workflow_harness(
|
||||
settings=implementation_settings(tmp_path),
|
||||
repository=repository,
|
||||
gitea=gitea,
|
||||
changed=False,
|
||||
responses=[
|
||||
AgentResult(
|
||||
summary_markdown="# Implement widget\n\nHandled empty input.",
|
||||
tests=["pytest: passed"],
|
||||
),
|
||||
ReviewReport(summary="Ready", findings=[]),
|
||||
],
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
JobRejected,
|
||||
match="OpenCode completed without producing any file changes",
|
||||
):
|
||||
await implement(job(), run, services)
|
||||
await implement(job(), harness.run, harness.services)
|
||||
|
||||
assert [call[0] for call in git.calls] == [
|
||||
assert [call[0] for call in harness.git.calls] == [
|
||||
"clone",
|
||||
"create_branch",
|
||||
"has_changes",
|
||||
@@ -355,7 +253,7 @@ async def test_initial_implementation_rejects_clean_worktree_before_commit_or_pr
|
||||
assert gitea.created_pulls == []
|
||||
assert repository.saved_workflows
|
||||
assert all(item.status is WorkflowStatus.ACTIVE for item in repository.saved_workflows)
|
||||
assert "creating pull request" not in run.stages
|
||||
assert "creating pull request" not in harness.run.stages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -376,17 +274,19 @@ async def test_initial_implementation_rejects_duplicate_agent_pr_before_clone(
|
||||
existing_pull: PullRequestInfo,
|
||||
message: str,
|
||||
) -> None:
|
||||
services, _, gitea, git, development, _, opencode = make_services(
|
||||
tmp_path,
|
||||
implementations=[workflow(pr_number=8)],
|
||||
existing_pulls={8: existing_pull},
|
||||
repository = FakeRepository(implementations=[workflow(pr_number=8)])
|
||||
gitea = FakeGitea(existing_pulls={8: existing_pull})
|
||||
harness = make_workflow_harness(
|
||||
settings=implementation_settings(tmp_path),
|
||||
repository=repository,
|
||||
gitea=gitea,
|
||||
)
|
||||
|
||||
with pytest.raises(JobRejected) as error:
|
||||
await implement(job(), RecordingRun(), services)
|
||||
await implement(job(), harness.run, harness.services)
|
||||
|
||||
assert str(error.value) == message
|
||||
assert git.calls == []
|
||||
assert development.workspaces == []
|
||||
assert opencode.resume_calls == []
|
||||
assert harness.git.calls == []
|
||||
assert harness.development.workspaces == []
|
||||
assert harness.opencode.resume_calls == []
|
||||
assert gitea.default_branch_calls == []
|
||||
|
||||
Reference in New Issue
Block a user