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