refactor tests
Publish container image / Build and push (push) Successful in 32s

This commit is contained in:
2026-07-26 23:49:40 +02:00
parent 5ef10d28fe
commit ce9f1e3d20
32 changed files with 1546 additions and 1923 deletions
+92 -184
View File
@@ -1,34 +1,18 @@
import json
from collections.abc import Iterable
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
from typing import Any
import pytest
from pydantic import BaseModel
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.model import AgentResult, ReviewReport
from agentci.workflows.pull_request import fix_pull_request, iterate_implementation
from agentci.workflows.render import JobRejected
from agentci.workflows.services import WorkflowServices
class RecordingRun(JobRun):
def __init__(self) -> None:
self.stages: list[str] = []
self.linked_workflows: list[tuple[str, str]] = []
self.linked_sessions: list[str] = []
async def stage(self, stage: str) -> None:
self.stages.append(stage)
async def link_workflow(self, workflow_id: str, stage: str) -> None:
self.linked_workflows.append((workflow_id, stage))
async def link_session(self, session_id: str) -> None:
self.linked_sessions.append(session_id)
from tests.workflow_support import WorkflowHarness, make_workflow_harness
class FakeRepository:
@@ -88,70 +72,6 @@ class FakeGitea:
return IssueInfo(7, "Fix widget", "The widget is broken.", "open")
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 "head-sha"
async def sync_branch(self, workspace: Path, branch: str) -> str:
self.calls.append(("sync_branch", workspace, branch))
return "head-sha"
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 "new-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, responses: list[BaseModel] | None = None) -> None:
self.responses = list(responses or [])
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) -> BaseModel:
self.resume_calls.append(values)
response = self.responses.pop(0)
assert isinstance(response, values["result_type"])
return response
def job(kind: JobKind, *, pr_number: int | None = 12) -> Job:
return Job(
id="job-pr",
@@ -231,46 +151,28 @@ def pull(
)
def make_services(
def pull_request_harness(
tmp_path: Path,
repository: FakeRepository,
gitea: FakeGitea,
*,
workflow: Workflow | None = None,
pull_info: PullRequestInfo | None = None,
plan: Workflow | None = None,
changed: bool = True,
responses: list[BaseModel] | None = None,
) -> tuple[
WorkflowServices,
FakeRepository,
FakeGitea,
RecordingGit,
RecordingDevelopment,
RecordingPrompts,
RecordingOpenCode,
]:
repository = FakeRepository(workflow=workflow, plan=plan)
gitea = FakeGitea(pull_info or pull())
git = RecordingGit(changed=changed)
development = RecordingDevelopment()
prompts = RecordingPrompts()
opencode = RecordingOpenCode(responses)
services = cast(
WorkflowServices,
SimpleNamespace(
settings=SimpleNamespace(
workspaces_dir=tmp_path / "workspaces",
implement_model="provider/model",
implement_variant="high",
),
repository=repository,
gitea=gitea,
git=git,
development=development,
prompts=prompts,
opencode=opencode,
responses: Iterable[BaseModel] = (),
) -> WorkflowHarness:
return make_workflow_harness(
settings=SimpleNamespace(
workspaces_dir=tmp_path / "workspaces",
implement_model="provider/model",
implement_variant="high",
),
repository=repository,
gitea=gitea,
changed=changed,
responses=responses,
clone_sha="head-sha",
sync_sha="head-sha",
commit_sha="new-sha",
)
return services, repository, gitea, git, development, prompts, opencode
def result(summary: str = "# Refine widget") -> AgentResult:
@@ -281,37 +183,52 @@ async def test_iterate_implementation_reuses_sessions_reviews_commits_and_pushes
tmp_path: Path,
) -> None:
existing = implementation_workflow()
services, repository, _, git, development, prompts, opencode = make_services(
repository = FakeRepository(workflow=existing, plan=plan_workflow())
gitea = FakeGitea(pull())
harness = pull_request_harness(
tmp_path,
workflow=existing,
plan=plan_workflow(),
repository,
gitea,
responses=[result(), ReviewReport(summary="Ready", findings=[])],
)
run = RecordingRun()
body = await iterate_implementation(job(JobKind.ITERATE_IMPLEMENT), run, services)
body = await iterate_implementation(
job(JobKind.ITERATE_IMPLEMENT),
harness.run,
harness.services,
)
assert run.linked_workflows == [(existing.id, "synchronizing branch")]
assert development.workspaces == [existing.workspace_path]
assert opencode.created_sessions == []
assert [call["session_id"] for call in opencode.resume_calls] == [
assert harness.run.linked_workflows == [(existing.id, "synchronizing branch")]
assert harness.development.workspaces == [existing.workspace_path]
assert harness.opencode.created_sessions == []
assert [call["session_id"] for call in harness.opencode.resume_calls] == [
"primary-session",
"reviewer-session",
]
assert [call["result_type"] for call in opencode.resume_calls] == [
assert [call["result_type"] for call in harness.opencode.resume_calls] == [
AgentResult,
ReviewReport,
]
iterate_prompt = prompts.calls[0]
iterate_prompt = harness.prompts.calls[0]
assert iterate_prompt[0] == "implementation_iterate"
assert iterate_prompt[1]["message"] == "Handle the review."
assert "Handle empty input." in iterate_prompt[1]["context"]
review_prompt = prompts.calls[1]
review_prompt = harness.prompts.calls[1]
assert review_prompt[0] == "implementation_review"
assert set(review_prompt[1]) == {"issue_context", "artifact", "pull_context"}
assert review_prompt[1]["issue_context"].startswith("Repository: org/repo\n")
assert review_prompt[1]["artifact"] == "Canonical plan"
assert "The widget is broken." in review_prompt[1]["issue_context"]
assert review_prompt[1]["pull_context"] == iterate_prompt[1]["context"]
assert harness.opencode.resume_calls[1] == {
"session_id": "reviewer-session",
"prompt": "rendered implementation_review",
"model": "provider/model",
"variant": "high",
"workspace": existing.workspace_path,
"schema_name": "review.json",
"result_type": ReviewReport,
}
assert git.calls == [
assert harness.git.calls == [
("sync_branch", existing.workspace_path, "agent/issue-7"),
("has_changes", existing.workspace_path),
("diff_check", existing.workspace_path),
@@ -368,23 +285,23 @@ async def test_iterate_implementation_rejects_missing_stale_or_incompatible_work
existing: Workflow | None,
message: str,
) -> None:
services, repository, gitea, git, development, _, opencode = make_services(
tmp_path, workflow=existing
)
repository = FakeRepository(workflow=existing)
gitea = FakeGitea(pull())
harness = pull_request_harness(tmp_path, repository, gitea)
with pytest.raises(JobRejected) as error:
await iterate_implementation(
job(JobKind.ITERATE_IMPLEMENT, pr_number=pr_number),
RecordingRun(),
services,
harness.run,
harness.services,
)
assert str(error.value) == message
assert repository.saved_workflows == []
assert gitea.pull_calls == []
assert git.calls == []
assert development.workspaces == []
assert opencode.resume_calls == []
assert harness.git.calls == []
assert harness.development.workspaces == []
assert harness.opencode.resume_calls == []
@pytest.mark.parametrize(
@@ -409,46 +326,51 @@ async def test_iterate_implementation_rejects_closed_or_stale_branch_before_chec
existing: Workflow,
message: str,
) -> None:
services, repository, _, git, development, _, opencode = make_services(
tmp_path, workflow=existing, pull_info=pull_info
)
repository = FakeRepository(workflow=existing)
harness = pull_request_harness(tmp_path, repository, FakeGitea(pull_info))
with pytest.raises(JobRejected) as error:
await iterate_implementation(job(JobKind.ITERATE_IMPLEMENT), RecordingRun(), services)
await iterate_implementation(
job(JobKind.ITERATE_IMPLEMENT),
harness.run,
harness.services,
)
assert str(error.value) == message
assert repository.saved_workflows == []
assert git.calls == []
assert development.workspaces == []
assert opencode.resume_calls == []
assert harness.git.calls == []
assert harness.development.workspaces == []
assert harness.opencode.resume_calls == []
async def test_fix_pull_request_clones_head_runs_agent_commits_and_pushes(
tmp_path: Path,
) -> None:
services, repository, _, git, development, prompts, opencode = make_services(
repository = FakeRepository()
harness = pull_request_harness(
tmp_path,
repository,
FakeGitea(pull()),
responses=[result("Fix empty input")],
)
run = RecordingRun()
body = await fix_pull_request(job(JobKind.FIX), run, services)
body = await fix_pull_request(job(JobKind.FIX), harness.run, harness.services)
workspace = tmp_path / "workspaces" / "fix-job-pr" / "repo"
assert git.calls == [
assert harness.git.calls == [
("clone", "contributor", "fork", "agent/issue-7", workspace),
("has_changes", workspace),
("diff_check", workspace),
("commit", workspace, "agent fix: Fix empty input"),
("push", workspace, "agent/issue-7", False),
]
assert development.workspaces == [workspace]
assert opencode.created_sessions == [(workspace, "fix")]
assert run.linked_sessions == ["fix-session"]
assert opencode.resume_calls[0]["session_id"] == "fix-session"
assert opencode.resume_calls[0]["result_type"] is AgentResult
assert prompts.calls[0][0] == "fix"
assert "`src/widget.py:8`: Add a guard." in prompts.calls[0][1]["context"]
assert harness.development.workspaces == [workspace]
assert harness.opencode.created_sessions == [(workspace, "fix")]
assert harness.run.linked_sessions == ["fix-session"]
assert harness.opencode.resume_calls[0]["session_id"] == "fix-session"
assert harness.opencode.resume_calls[0]["result_type"] is AgentResult
assert harness.prompts.calls[0][0] == "fix"
assert "`src/widget.py:8`: Add a guard." in harness.prompts.calls[0][1]["context"]
assert repository.saved_workflows == []
assert body == (
"<!-- agentci:fix workflow=job-pr -->\n"
@@ -457,32 +379,13 @@ async def test_fix_pull_request_clones_head_runs_agent_commits_and_pushes(
)
async def test_fix_pull_request_rejects_clean_worktree_before_push(
tmp_path: Path,
) -> None:
services, _, _, git, _, _, _ = make_services(
tmp_path,
changed=False,
responses=[result("No changes required")],
)
with pytest.raises(
JobRejected,
match="OpenCode completed without producing any file changes",
):
await fix_pull_request(job(JobKind.FIX), RecordingRun(), services)
assert [call[0] for call in git.calls] == ["clone", "has_changes"]
@pytest.mark.parametrize(
("pr_number", "pull_info", "message"),
[
(None, pull(), "This command requires a pull request."),
(12, pull(state="closed"), "Fixes require an open pull request."),
(12, pull(state="closed", merged=True), "Fixes require an open pull request."),
],
ids=["missing-pr", "closed", "merged"],
ids=["missing-pr", "closed"],
)
async def test_fix_pull_request_requires_open_pr_before_clone(
tmp_path: Path,
@@ -490,13 +393,18 @@ async def test_fix_pull_request_requires_open_pr_before_clone(
pull_info: PullRequestInfo,
message: str,
) -> None:
services, _, gitea, git, development, _, opencode = make_services(tmp_path, pull_info=pull_info)
gitea = FakeGitea(pull_info)
harness = pull_request_harness(tmp_path, FakeRepository(), gitea)
with pytest.raises(JobRejected) as error:
await fix_pull_request(job(JobKind.FIX, pr_number=pr_number), RecordingRun(), services)
await fix_pull_request(
job(JobKind.FIX, pr_number=pr_number),
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.pull_calls == ([] if pr_number is None else [("org", "repo", 12)])