This commit is contained in:
+63
-97
@@ -20,7 +20,6 @@ from agentci.engine.events import (
|
||||
JobRejected as RejectedEvent,
|
||||
)
|
||||
from agentci.engine.model import (
|
||||
IncomingCommand,
|
||||
Job,
|
||||
JobKind,
|
||||
JobStatus,
|
||||
@@ -31,12 +30,9 @@ from agentci.engine.model import (
|
||||
WorkflowKind,
|
||||
)
|
||||
from agentci.engine.reducer import render_job_comment
|
||||
from agentci.engine.repository import Repository
|
||||
from agentci.integrations.gitea.models import CommentInfo
|
||||
from agentci.workflows.render import JobRejected
|
||||
|
||||
MIGRATIONS = Path(__file__).parents[1] / "src" / "agentci" / "migrations"
|
||||
|
||||
|
||||
class FakeRepository:
|
||||
def __init__(
|
||||
@@ -54,7 +50,6 @@ class FakeRepository:
|
||||
self.events: list[tuple[str, object]] = []
|
||||
self.operations: list[str] = []
|
||||
self.running: list[Job] = []
|
||||
self.failed_workflows: list[str] = []
|
||||
self.complete_stop: asyncio.Event | None = None
|
||||
|
||||
async def claim_task(self, queue: QueueName) -> Task | None:
|
||||
@@ -91,9 +86,6 @@ class FakeRepository:
|
||||
async def get_workflow(self, _workflow_id: str) -> Workflow | None:
|
||||
return self.workflow
|
||||
|
||||
async def fail_job_workflow(self, job_id: str) -> None:
|
||||
self.failed_workflows.append(job_id)
|
||||
|
||||
|
||||
class FakeGitea:
|
||||
def __init__(self, *, permitted: bool = True) -> None:
|
||||
@@ -181,7 +173,7 @@ def task(
|
||||
|
||||
def make_worker(
|
||||
tmp_path: Path,
|
||||
repository: FakeRepository | Repository,
|
||||
repository: FakeRepository,
|
||||
*,
|
||||
gitea: FakeGitea | None = None,
|
||||
opencode: FakeOpenCode | None = None,
|
||||
@@ -329,74 +321,47 @@ async def test_run_recovers_before_starting_configured_consumers(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("permitted", "event_type", "suffix"),
|
||||
("status", "permitted", "expected_events", "expected_permission_calls"),
|
||||
[
|
||||
(True, PermissionGranted, "permission-granted"),
|
||||
(False, PermissionDenied, "permission-denied"),
|
||||
pytest.param(
|
||||
JobStatus.RECEIVED,
|
||||
True,
|
||||
[("task:7:permission-granted", PermissionGranted(job_id="job"))],
|
||||
[("org", "repo", "alice")],
|
||||
id="received-permitted",
|
||||
),
|
||||
pytest.param(
|
||||
JobStatus.RECEIVED,
|
||||
False,
|
||||
[("task:7:permission-denied", PermissionDenied(job_id="job"))],
|
||||
[("org", "repo", "alice")],
|
||||
id="received-denied",
|
||||
),
|
||||
pytest.param(
|
||||
JobStatus.QUEUED,
|
||||
True,
|
||||
[],
|
||||
[],
|
||||
id="queued",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_authorize_records_permission_outcome(
|
||||
async def test_handle_authorization_obeys_job_state_and_permission(
|
||||
tmp_path: Path,
|
||||
status: JobStatus,
|
||||
permitted: bool,
|
||||
event_type: type[PermissionGranted] | type[PermissionDenied],
|
||||
suffix: str,
|
||||
expected_events: list[tuple[str, object]],
|
||||
expected_permission_calls: list[tuple[str, str, str]],
|
||||
) -> None:
|
||||
current = job()
|
||||
repository = FakeRepository(current)
|
||||
repository = FakeRepository(job(status=status))
|
||||
gitea = FakeGitea(permitted=permitted)
|
||||
authorize = task(TaskKind.AUTHORIZE, QueueName.CONTROL)
|
||||
|
||||
await make_worker(tmp_path, repository, gitea=gitea)._authorize(authorize, current)
|
||||
|
||||
assert gitea.permission_calls == [("org", "repo", "alice")]
|
||||
assert repository.events[0][0] == f"task:{authorize.id}:{suffix}"
|
||||
assert isinstance(repository.events[0][1], event_type)
|
||||
|
||||
|
||||
async def test_authorize_ignores_job_after_received_status(tmp_path: Path) -> None:
|
||||
current = job(status=JobStatus.QUEUED)
|
||||
repository = FakeRepository(current)
|
||||
gitea = FakeGitea()
|
||||
|
||||
await make_worker(tmp_path, repository, gitea=gitea)._authorize(
|
||||
task(TaskKind.AUTHORIZE, QueueName.CONTROL), current
|
||||
await make_worker(tmp_path, repository, gitea=gitea)._handle(
|
||||
task(TaskKind.AUTHORIZE, QueueName.CONTROL)
|
||||
)
|
||||
|
||||
assert gitea.permission_calls == []
|
||||
assert repository.events == []
|
||||
|
||||
|
||||
async def test_authorize_integrates_with_real_repository(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = Repository(tmp_path / "state.sqlite3", MIGRATIONS)
|
||||
await repository.initialize()
|
||||
accepted = await repository.accept(
|
||||
IncomingCommand(
|
||||
delivery_id="delivery-real",
|
||||
comment_id=1,
|
||||
repo_owner="org",
|
||||
repo_name="repo",
|
||||
issue_number=1,
|
||||
pr_number=None,
|
||||
requester="alice",
|
||||
body="/agent plan write tests",
|
||||
)
|
||||
)
|
||||
authorize = await repository.claim_task(QueueName.CONTROL)
|
||||
assert authorize is not None
|
||||
assert authorize.kind is TaskKind.AUTHORIZE
|
||||
|
||||
await make_worker(tmp_path, repository)._handle(authorize)
|
||||
|
||||
persisted = await repository.get_job(accepted.job.id)
|
||||
assert persisted is not None
|
||||
assert persisted.status is JobStatus.QUEUED
|
||||
assert persisted.kind is JobKind.PLAN
|
||||
assert persisted.message == "write tests"
|
||||
execute = await repository.claim_task(QueueName.JOBS)
|
||||
assert execute is not None
|
||||
assert execute.kind is TaskKind.EXECUTE
|
||||
assert repository.events == expected_events
|
||||
assert gitea.permission_calls == expected_permission_calls
|
||||
|
||||
|
||||
async def test_execute_completes_with_workflow_comment(
|
||||
@@ -603,43 +568,44 @@ async def test_abort_collects_and_deduplicates_workflow_sessions(
|
||||
assert set(opencode.aborted) == {(session, workspace) for session in expected}
|
||||
|
||||
|
||||
async def test_abort_uses_one_shot_workspace_without_workflow(tmp_path: Path) -> None:
|
||||
opencode = FakeOpenCode()
|
||||
|
||||
await make_worker(tmp_path, FakeRepository(), opencode=opencode)._abort_job_sessions(
|
||||
job(workflow_id="missing", session_id="session")
|
||||
)
|
||||
|
||||
assert opencode.aborted == [("session", tmp_path / "fix-job" / "repo")]
|
||||
|
||||
|
||||
async def test_abort_does_not_mix_one_shot_session_into_existing_workflow(
|
||||
@pytest.mark.parametrize(
|
||||
("workflow_id", "session_id", "has_workflow", "uses_one_shot"),
|
||||
[
|
||||
pytest.param("missing", "session", False, True, id="one-shot-fallback"),
|
||||
pytest.param("flow", "one-shot", True, False, id="workflow-precedence"),
|
||||
pytest.param(None, None, False, False, id="no-sessions"),
|
||||
],
|
||||
)
|
||||
async def test_abort_session_source_precedence(
|
||||
tmp_path: Path,
|
||||
workflow_id: str | None,
|
||||
session_id: str | None,
|
||||
has_workflow: bool,
|
||||
uses_one_shot: bool,
|
||||
) -> None:
|
||||
workflow = Workflow(
|
||||
id="flow",
|
||||
kind=WorkflowKind.IMPLEMENT,
|
||||
repo_owner="org",
|
||||
repo_name="repo",
|
||||
issue_number=1,
|
||||
workspace_path=tmp_path / "workflow" / "repo",
|
||||
base_sha="base",
|
||||
persisted = (
|
||||
Workflow(
|
||||
id="flow",
|
||||
kind=WorkflowKind.IMPLEMENT,
|
||||
repo_owner="org",
|
||||
repo_name="repo",
|
||||
issue_number=1,
|
||||
workspace_path=tmp_path / "workflow" / "repo",
|
||||
base_sha="base",
|
||||
)
|
||||
if has_workflow
|
||||
else None
|
||||
)
|
||||
opencode = FakeOpenCode()
|
||||
|
||||
await make_worker(
|
||||
tmp_path, FakeRepository(workflow=workflow), opencode=opencode
|
||||
)._abort_job_sessions(job(workflow_id="flow", session_id="one-shot"))
|
||||
tmp_path,
|
||||
FakeRepository(workflow=persisted),
|
||||
opencode=opencode,
|
||||
)._abort_job_sessions(job(workflow_id=workflow_id, session_id=session_id))
|
||||
|
||||
assert opencode.aborted == []
|
||||
|
||||
|
||||
async def test_abort_without_persisted_sessions_is_noop(tmp_path: Path) -> None:
|
||||
opencode = FakeOpenCode()
|
||||
|
||||
await make_worker(tmp_path, FakeRepository(), opencode=opencode)._abort_job_sessions(job())
|
||||
|
||||
assert opencode.aborted == []
|
||||
expected = [("session", tmp_path / "fix-job" / "repo")] if uses_one_shot else []
|
||||
assert opencode.aborted == expected
|
||||
|
||||
|
||||
def test_safe_error_is_single_line_and_bounded() -> None:
|
||||
|
||||
Reference in New Issue
Block a user