This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from agentci.engine.model import Workflow
|
||||
from agentci.engine.run import JobRun
|
||||
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)
|
||||
|
||||
|
||||
class ScriptedOpenCode:
|
||||
def __init__(
|
||||
self,
|
||||
responses: Iterable[BaseModel] = (),
|
||||
trace: list[tuple[object, ...]] | None = None,
|
||||
) -> None:
|
||||
self.responses = list(responses)
|
||||
self.created_sessions: list[tuple[Path, str]] = []
|
||||
self.resume_calls: list[dict[str, Any]] = []
|
||||
self.trace = trace if trace is not None else []
|
||||
|
||||
async def create_session(self, workspace: Path, title: str) -> str:
|
||||
session_id = f"{title}-session"
|
||||
self.created_sessions.append((workspace, title))
|
||||
self.trace.append(("create_session", title, session_id))
|
||||
return session_id
|
||||
|
||||
async def resume(self, **values: Any) -> BaseModel:
|
||||
self.resume_calls.append(values)
|
||||
self.trace.append(("resume", values["session_id"], values["result_type"]))
|
||||
assert self.responses, "unexpected OpenCode resume call"
|
||||
response = self.responses.pop(0)
|
||||
assert isinstance(response, values["result_type"]), (
|
||||
f"expected {values['result_type'].__name__}, got {type(response).__name__}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
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 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 RecordingGit:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
changed: bool = True,
|
||||
clone_sha: str = "base-sha",
|
||||
sync_sha: str = "head-sha",
|
||||
commit_sha: str = "commit-sha",
|
||||
) -> None:
|
||||
self.changed = changed
|
||||
self.clone_sha = clone_sha
|
||||
self.sync_sha = sync_sha
|
||||
self.commit_sha = commit_sha
|
||||
self.calls: list[tuple[object, ...]] = []
|
||||
|
||||
async def clone(self, owner: str, repo: str, branch: str, destination: Path) -> str:
|
||||
self.calls.append(("clone", owner, repo, branch, destination))
|
||||
return self.clone_sha
|
||||
|
||||
async def create_branch(self, workspace: Path, branch: str) -> None:
|
||||
self.calls.append(("create_branch", workspace, branch))
|
||||
|
||||
async def sync_branch(self, workspace: Path, branch: str) -> str:
|
||||
self.calls.append(("sync_branch", workspace, branch))
|
||||
return self.sync_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 self.commit_sha
|
||||
|
||||
async def push(self, workspace: Path, branch: str, *, set_upstream: bool = False) -> None:
|
||||
self.calls.append(("push", workspace, branch, set_upstream))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WorkflowHarness:
|
||||
services: WorkflowServices
|
||||
run: RecordingRun
|
||||
git: RecordingGit
|
||||
development: RecordingDevelopment
|
||||
prompts: RecordingPrompts
|
||||
opencode: ScriptedOpenCode
|
||||
trace: list[tuple[object, ...]]
|
||||
|
||||
|
||||
def make_workflow_harness(
|
||||
*,
|
||||
settings: SimpleNamespace,
|
||||
repository: object,
|
||||
gitea: object,
|
||||
responses: Iterable[BaseModel] = (),
|
||||
trace: list[tuple[object, ...]] | None = None,
|
||||
changed: bool = True,
|
||||
clone_sha: str = "base-sha",
|
||||
sync_sha: str = "head-sha",
|
||||
commit_sha: str = "commit-sha",
|
||||
) -> WorkflowHarness:
|
||||
shared_trace = trace if trace is not None else []
|
||||
run = RecordingRun()
|
||||
git = RecordingGit(
|
||||
changed=changed,
|
||||
clone_sha=clone_sha,
|
||||
sync_sha=sync_sha,
|
||||
commit_sha=commit_sha,
|
||||
)
|
||||
development = RecordingDevelopment()
|
||||
prompts = RecordingPrompts()
|
||||
opencode = ScriptedOpenCode(responses, shared_trace)
|
||||
services = cast(
|
||||
WorkflowServices,
|
||||
SimpleNamespace(
|
||||
settings=settings,
|
||||
repository=repository,
|
||||
gitea=gitea,
|
||||
git=git,
|
||||
development=development,
|
||||
prompts=prompts,
|
||||
opencode=opencode,
|
||||
),
|
||||
)
|
||||
return WorkflowHarness(
|
||||
services=services,
|
||||
run=run,
|
||||
git=git,
|
||||
development=development,
|
||||
prompts=prompts,
|
||||
opencode=opencode,
|
||||
trace=shared_trace,
|
||||
)
|
||||
Reference in New Issue
Block a user