124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
import sqlite3
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from agentci.adapters.storage import Storage
|
|
from agentci.domain.events import (
|
|
JobStarted,
|
|
PermissionDenied,
|
|
PermissionGranted,
|
|
WorkflowCreated,
|
|
)
|
|
from agentci.domain.models import CommandEvent, Workflow, WorkflowKind, WorkflowStatus
|
|
from agentci.state_machine import StateMachine
|
|
|
|
|
|
@pytest.fixture
|
|
async def storage(tmp_path: Path) -> Storage:
|
|
migrations = Path(__file__).parents[1] / "src" / "agentci" / "migrations"
|
|
value = Storage(tmp_path / "state.sqlite3", migrations)
|
|
await value.initialize()
|
|
return value
|
|
|
|
|
|
def command(delivery: str, body: str = "/agent plan") -> CommandEvent:
|
|
return CommandEvent(
|
|
delivery_id=delivery,
|
|
comment_id=int(delivery.rsplit("-", 1)[-1]),
|
|
repo_owner="alice",
|
|
repo_name="repo",
|
|
issue_number=3,
|
|
pr_number=None,
|
|
requester="alice",
|
|
body=body,
|
|
)
|
|
|
|
|
|
async def test_receive_is_idempotent_without_consuming_sequence(storage: Storage) -> None:
|
|
host = StateMachine(storage)
|
|
first = await host.receive(command("delivery-1"))
|
|
duplicate = await host.receive(command("delivery-1"))
|
|
second = await host.receive(command("delivery-2"))
|
|
|
|
assert not first.duplicate
|
|
assert duplicate.duplicate
|
|
assert duplicate.state.id == first.state.id
|
|
assert second.state.receive_sequence == first.state.receive_sequence + 1
|
|
|
|
|
|
async def test_received_job_blocks_later_execute_task(storage: Storage) -> None:
|
|
host = StateMachine(storage)
|
|
first = (await host.receive(command("delivery-1"))).state
|
|
second = (await host.receive(command("delivery-2"))).state
|
|
await host.evolve("grant-2", PermissionGranted(job_id=second.id))
|
|
|
|
assert await storage.claim_task("jobs") is None
|
|
|
|
await host.evolve("deny-1", PermissionDenied(job_id=first.id))
|
|
task = await storage.claim_task("jobs")
|
|
assert task is not None
|
|
assert task.job_id == second.id
|
|
|
|
|
|
async def test_started_and_finished_timestamps_are_owned_by_store(storage: Storage) -> None:
|
|
host = StateMachine(storage)
|
|
state = (await host.receive(command("delivery-1"))).state
|
|
state = (await host.evolve("grant", PermissionGranted(job_id=state.id))).state
|
|
state = (await host.evolve("start", JobStarted(job_id=state.id))).state
|
|
with sqlite3.connect(storage.database_path) as connection:
|
|
row = connection.execute(
|
|
"SELECT started_at, finished_at FROM jobs WHERE id=?", (state.id,)
|
|
).fetchone()
|
|
assert row is not None
|
|
assert row[0] is not None
|
|
assert row[1] is None
|
|
|
|
|
|
async def test_workflow_queries_and_completed_protection(storage: Storage, tmp_path: Path) -> None:
|
|
workflow = Workflow(
|
|
id="workflow-1",
|
|
kind=WorkflowKind.PLAN,
|
|
repo_owner="alice",
|
|
repo_name="repo",
|
|
issue_number=3,
|
|
workspace_path=tmp_path / "repo",
|
|
base_sha="abc",
|
|
artifact="# Plan",
|
|
status=WorkflowStatus.COMPLETED,
|
|
)
|
|
await storage.create_workflow(workflow)
|
|
await storage.fail_job_workflow("missing-job")
|
|
loaded = await storage.latest_workflow("alice", "repo", 3, WorkflowKind.PLAN)
|
|
assert loaded is not None
|
|
assert loaded.status is WorkflowStatus.COMPLETED
|
|
|
|
|
|
async def test_workflow_creation_and_job_link_are_atomic(storage: Storage, tmp_path: Path) -> None:
|
|
host = StateMachine(storage)
|
|
state = (await host.receive(command("delivery-1"))).state
|
|
state = (await host.evolve("grant", PermissionGranted(job_id=state.id))).state
|
|
state = (await host.evolve("start", JobStarted(job_id=state.id))).state
|
|
workflow = Workflow(
|
|
id="workflow-atomic",
|
|
kind=WorkflowKind.PLAN,
|
|
repo_owner="alice",
|
|
repo_name="repo",
|
|
issue_number=3,
|
|
workspace_path=tmp_path / "repo",
|
|
base_sha="abc",
|
|
)
|
|
result = await host.evolve(
|
|
"workflow-created",
|
|
WorkflowCreated(job_id=state.id, workflow=workflow, stage="planning"),
|
|
)
|
|
assert result.state.workflow_id == workflow.id
|
|
assert await storage.get_workflow(workflow.id) is not None
|
|
|
|
|
|
async def test_schema_has_receive_sequence_and_no_version(storage: Storage) -> None:
|
|
with sqlite3.connect(storage.database_path) as connection:
|
|
columns = {row[1] for row in connection.execute("PRAGMA table_info(jobs)")}
|
|
assert "receive_sequence" in columns
|
|
assert "version" not in columns
|