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
+74 -139
View File
@@ -1,38 +1,15 @@
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
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 DiscussionReply, PlanArtifact, ReviewReport
from agentci.workflows.plan import create_plan, discuss_plan, iterate_plan
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_workflows: list[tuple[str, 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_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 make_workflow_harness
class FakeRepository:
@@ -86,41 +63,6 @@ class FakeGitea:
return self.pulls[number]
class RecordingGit:
def __init__(self) -> None:
self.clone_calls: list[tuple[str, str, str, Path]] = []
async def clone(self, owner: str, repo: str, branch: str, destination: Path) -> str:
self.clone_calls.append((owner, repo, branch, destination))
return "base-sha"
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, *, message: str | None = "Please be specific.") -> Job:
return Job(
id="job-plan",
@@ -192,78 +134,50 @@ def pull(*, state: str = "open", merged: bool = False) -> PullRequestInfo:
)
def make_services(
tmp_path: Path,
*,
latest: Workflow | None = None,
implementations: list[Workflow] | None = None,
pulls: dict[int, PullRequestInfo] | None = None,
responses: list[BaseModel] | None = None,
) -> tuple[
WorkflowServices,
FakeRepository,
FakeGitea,
RecordingGit,
RecordingPrompts,
RecordingOpenCode,
]:
repository = FakeRepository(latest=latest, implementations=implementations)
gitea = FakeGitea(pulls)
git = RecordingGit()
prompts = RecordingPrompts()
opencode = RecordingOpenCode(responses)
services = cast(
WorkflowServices,
SimpleNamespace(
settings=SimpleNamespace(
workspaces_dir=tmp_path / "workspaces",
plan_model="provider/model",
plan_variant="high",
plan_review_rounds=3,
),
repository=repository,
gitea=gitea,
git=git,
prompts=prompts,
opencode=opencode,
),
def plan_settings(tmp_path: Path) -> SimpleNamespace:
return SimpleNamespace(
workspaces_dir=tmp_path / "workspaces",
plan_model="provider/model",
plan_variant="high",
plan_review_rounds=3,
)
return services, repository, gitea, git, prompts, opencode
async def test_create_plan_completes_primary_and_independent_review(
tmp_path: Path,
) -> None:
services, repository, gitea, git, prompts, opencode = make_services(
tmp_path,
repository = FakeRepository()
gitea = FakeGitea()
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=gitea,
responses=[
PlanArtifact(plan_markdown="# Complete plan"),
ReviewReport(summary="Ready", findings=[]),
],
)
run = RecordingRun()
body = await create_plan(job(JobKind.PLAN), run, services)
body = await create_plan(job(JobKind.PLAN), harness.run, harness.services)
created, stage = run.created_workflows[0]
created, stage = harness.run.created_workflows[0]
assert stage == "planning"
assert created.kind is WorkflowKind.PLAN
assert created.workspace_path == tmp_path / "workspaces" / created.id / "repo"
assert git.clone_calls == [("org", "repo", "trunk", created.workspace_path)]
assert harness.git.calls == [("clone", "org", "repo", "trunk", created.workspace_path)]
assert gitea.default_branch_calls == [("org", "repo")]
assert run.linked_sessions == ["plan-session"]
assert opencode.created_sessions == [
assert harness.run.linked_sessions == ["plan-session"]
assert harness.opencode.created_sessions == [
(created.workspace_path, "plan"),
(created.workspace_path, "plan-review"),
]
assert [call["result_type"] for call in opencode.resume_calls] == [
assert [call["result_type"] for call in harness.opencode.resume_calls] == [
PlanArtifact,
ReviewReport,
]
assert prompts.calls[0][0] == "plan_initial"
assert prompts.calls[0][1]["request"] == "Please be specific."
assert "Use the existing API." in prompts.calls[0][1]["context"]
assert "Job queued" not in prompts.calls[0][1]["context"]
assert harness.prompts.calls[0][0] == "plan_initial"
assert harness.prompts.calls[0][1]["request"] == "Please be specific."
assert harness.prompts.calls[0][1]["context"].startswith("Repository: org/repo\n")
completed = repository.saved_workflows[-1]
assert completed.status is WorkflowStatus.COMPLETED
@@ -281,20 +195,21 @@ async def test_discuss_plan_resumes_primary_session_without_replacing_artifact(
tmp_path: Path,
) -> None:
existing = plan_workflow()
services, repository, _, _, prompts, opencode = make_services(
tmp_path,
latest=existing,
repository = FakeRepository(latest=existing)
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=FakeGitea(),
responses=[DiscussionReply(markdown="The API remains compatible.")],
)
run = RecordingRun()
body = await discuss_plan(job(JobKind.DISCUSS), run, services)
body = await discuss_plan(job(JobKind.DISCUSS), harness.run, harness.services)
assert run.linked_workflows == [(existing.id, "discussing")]
assert opencode.created_sessions == []
assert opencode.resume_calls[0]["session_id"] == "primary-session"
assert opencode.resume_calls[0]["schema_name"] == "discussion.json"
assert prompts.calls == [
assert harness.run.linked_workflows == [(existing.id, "discussing")]
assert harness.opencode.created_sessions == []
assert harness.opencode.resume_calls[0]["session_id"] == "primary-session"
assert harness.opencode.resume_calls[0]["schema_name"] == "discussion.json"
assert harness.prompts.calls == [
(
"discuss",
{"artifact": "Original plan", "message": "Please be specific."},
@@ -327,45 +242,57 @@ async def test_discuss_plan_rejects_missing_or_incompatible_plan(
latest: Workflow | None,
message: str,
) -> None:
services, _, _, _, _, opencode = make_services(tmp_path, latest=latest)
repository = FakeRepository(latest=latest)
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=FakeGitea(),
)
with pytest.raises(JobRejected) as error:
await discuss_plan(job(JobKind.DISCUSS), RecordingRun(), services)
await discuss_plan(job(JobKind.DISCUSS), harness.run, harness.services)
assert str(error.value) == message
assert opencode.resume_calls == []
assert harness.opencode.resume_calls == []
async def test_iterate_plan_revises_then_reuses_reviewer_session(
tmp_path: Path,
) -> None:
existing = plan_workflow()
services, repository, gitea, _, prompts, opencode = make_services(
tmp_path,
repository = FakeRepository(
latest=existing,
implementations=[implementation_workflow(), implementation_workflow(None)],
pulls={9: pull(state="closed")},
)
gitea = FakeGitea({9: pull(state="closed")})
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=gitea,
responses=[
PlanArtifact(plan_markdown="# Revised plan"),
ReviewReport(summary="Ready", findings=[]),
],
)
run = RecordingRun()
body = await iterate_plan(job(JobKind.ITERATE_PLAN, message=None), run, services)
body = await iterate_plan(
job(JobKind.ITERATE_PLAN, message=None),
harness.run,
harness.services,
)
assert gitea.pull_calls == [9]
assert run.linked_workflows == [(existing.id, "iterating plan")]
assert opencode.created_sessions == []
assert [call["session_id"] for call in opencode.resume_calls] == [
assert harness.run.linked_workflows == [(existing.id, "iterating plan")]
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] == [
PlanArtifact,
ReviewReport,
]
iterate_prompt = prompts.calls[0]
iterate_prompt = harness.prompts.calls[0]
assert iterate_prompt[0] == "plan_iterate"
assert iterate_prompt[1]["artifact"] == "Original plan"
assert iterate_prompt[1]["message"] == ("(refine using the latest discussion and prior review)")
@@ -403,14 +330,19 @@ async def test_iterate_plan_rejects_missing_or_incompatible_plan(
latest: Workflow | None,
message: str,
) -> None:
services, repository, _, _, _, opencode = make_services(tmp_path, latest=latest)
repository = FakeRepository(latest=latest)
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=FakeGitea(),
)
with pytest.raises(JobRejected) as error:
await iterate_plan(job(JobKind.ITERATE_PLAN), RecordingRun(), services)
await iterate_plan(job(JobKind.ITERATE_PLAN), harness.run, harness.services)
assert str(error.value) == message
assert repository.saved_workflows == []
assert opencode.resume_calls == []
assert harness.opencode.resume_calls == []
@pytest.mark.parametrize(
@@ -421,19 +353,22 @@ async def test_iterate_plan_rejects_missing_or_incompatible_plan(
async def test_iterate_plan_rejects_after_agent_pr_is_open_or_merged(
tmp_path: Path, blocking_pull: PullRequestInfo
) -> None:
services, repository, _, _, _, opencode = make_services(
tmp_path,
repository = FakeRepository(
latest=plan_workflow(),
implementations=[implementation_workflow()],
pulls={9: blocking_pull},
)
harness = make_workflow_harness(
settings=plan_settings(tmp_path),
repository=repository,
gitea=FakeGitea({9: blocking_pull}),
)
with pytest.raises(JobRejected) as error:
await iterate_plan(job(JobKind.ITERATE_PLAN), RecordingRun(), services)
await iterate_plan(job(JobKind.ITERATE_PLAN), harness.run, harness.services)
assert str(error.value) == (
"Issue plan iteration is disabled because agent PR #9 is open or merged. "
"Iterate an open implementation on its PR."
)
assert repository.latest_calls == 0
assert opencode.resume_calls == []
assert harness.opencode.resume_calls == []