This commit is contained in:
@@ -1,24 +1,14 @@
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agentci.engine import _sqlite
|
||||
from agentci.engine import repository as repository_module
|
||||
from agentci.engine.events import JobStarted, PermissionGranted
|
||||
from agentci.engine.model import IncomingCommand, QueueName, Task, TaskKind
|
||||
from agentci.engine.repository import Repository
|
||||
|
||||
MIGRATIONS = Path(__file__).parents[1] / "src" / "agentci" / "migrations"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def repository(tmp_path: Path) -> Repository:
|
||||
value = Repository(tmp_path / "state.sqlite3", MIGRATIONS)
|
||||
await value.initialize()
|
||||
return value
|
||||
from tests.conftest import SQLiteClock
|
||||
|
||||
|
||||
def command(delivery: str, *, issue: int = 3) -> IncomingCommand:
|
||||
@@ -34,8 +24,8 @@ def command(delivery: str, *, issue: int = 3) -> IncomingCommand:
|
||||
)
|
||||
|
||||
|
||||
def task_storage(repository: Repository, task_id: int) -> tuple[object, ...]:
|
||||
with closing(sqlite3.connect(repository.database_path)) as connection, connection:
|
||||
def task_storage(engine_repository: Repository, task_id: int) -> tuple[object, ...]:
|
||||
with closing(sqlite3.connect(engine_repository.database_path)) as connection, connection:
|
||||
row = connection.execute(
|
||||
"""SELECT status, attempts, available_at, error, created_at, started_at, finished_at
|
||||
FROM listener_tasks WHERE id=?""",
|
||||
@@ -46,20 +36,18 @@ def task_storage(repository: Repository, task_id: int) -> tuple[object, ...]:
|
||||
|
||||
|
||||
async def test_claim_and_complete_record_attempt_and_lifecycle_timestamps(
|
||||
repository: Repository,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
engine_repository: Repository,
|
||||
sqlite_clock: SQLiteClock,
|
||||
) -> None:
|
||||
clock = {"now": "2026-02-01T00:00:00+00:00"}
|
||||
monkeypatch.setattr(_sqlite, "now", lambda: clock["now"])
|
||||
job = (await repository.accept(command("delivery-1"))).job
|
||||
clock["now"] = "2026-02-01T00:01:00+00:00"
|
||||
job = (await engine_repository.accept(command("delivery-1"))).job
|
||||
sqlite_clock.now = "2026-02-01T00:01:00+00:00"
|
||||
|
||||
task = await repository.claim_task(QueueName.CONTROL)
|
||||
task = await engine_repository.claim_task(QueueName.CONTROL)
|
||||
assert task is not None
|
||||
clock["now"] = "2026-02-01T00:02:00+00:00"
|
||||
await repository.complete_task(task.id)
|
||||
sqlite_clock.now = "2026-02-01T00:02:00+00:00"
|
||||
await engine_repository.complete_task(task.id)
|
||||
|
||||
assert (task, task_storage(repository, task.id)) == (
|
||||
assert (task, task_storage(engine_repository, task.id)) == (
|
||||
Task(
|
||||
id=task.id,
|
||||
job_id=job.id,
|
||||
@@ -82,17 +70,16 @@ async def test_claim_and_complete_record_attempt_and_lifecycle_timestamps(
|
||||
|
||||
@pytest.mark.parametrize(("attempts", "delay_seconds"), [(1, 2), (9, 256)])
|
||||
async def test_retry_uses_bounded_backoff_and_preserves_attempt_history_until_due(
|
||||
repository: Repository,
|
||||
engine_repository: Repository,
|
||||
sqlite_clock: SQLiteClock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
attempts: int,
|
||||
delay_seconds: int,
|
||||
) -> None:
|
||||
clock = {"now": "2026-02-01T00:00:00+00:00"}
|
||||
monkeypatch.setattr(_sqlite, "now", lambda: clock["now"])
|
||||
await repository.accept(command("delivery-1"))
|
||||
task = await repository.claim_task(QueueName.CONTROL)
|
||||
await engine_repository.accept(command("delivery-1"))
|
||||
task = await engine_repository.claim_task(QueueName.CONTROL)
|
||||
assert task is not None
|
||||
with closing(sqlite3.connect(repository.database_path)) as connection, connection:
|
||||
with closing(sqlite3.connect(engine_repository.database_path)) as connection, connection:
|
||||
connection.execute("UPDATE listener_tasks SET attempts=? WHERE id=?", (attempts, task.id))
|
||||
|
||||
retry_time = datetime(2026, 2, 1, 1, tzinfo=UTC)
|
||||
@@ -105,12 +92,12 @@ async def test_retry_uses_bounded_backoff_and_preserves_attempt_history_until_du
|
||||
|
||||
monkeypatch.setattr(repository_module, "datetime", FixedDateTime)
|
||||
error = "failure: " + "x" * 1100
|
||||
await repository.retry_task(task.id, attempts, error)
|
||||
pending_storage = task_storage(repository, task.id)
|
||||
clock["now"] = (available_at - timedelta(microseconds=1)).isoformat()
|
||||
early_claim = await repository.claim_task(QueueName.CONTROL)
|
||||
clock["now"] = available_at.isoformat()
|
||||
due_claim = await repository.claim_task(QueueName.CONTROL)
|
||||
await engine_repository.retry_task(task.id, attempts, error)
|
||||
pending_storage = task_storage(engine_repository, task.id)
|
||||
sqlite_clock.now = (available_at - timedelta(microseconds=1)).isoformat()
|
||||
early_claim = await engine_repository.claim_task(QueueName.CONTROL)
|
||||
sqlite_clock.now = available_at.isoformat()
|
||||
due_claim = await engine_repository.claim_task(QueueName.CONTROL)
|
||||
|
||||
assert (
|
||||
pending_storage,
|
||||
@@ -132,35 +119,33 @@ async def test_retry_uses_bounded_backoff_and_preserves_attempt_history_until_du
|
||||
|
||||
|
||||
async def test_recovery_requeues_control_and_unstarted_execution_but_fails_started_execution(
|
||||
repository: Repository,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
engine_repository: Repository,
|
||||
sqlite_clock: SQLiteClock,
|
||||
) -> None:
|
||||
clock = {"now": "2026-02-01T00:00:00+00:00"}
|
||||
monkeypatch.setattr(_sqlite, "now", lambda: clock["now"])
|
||||
|
||||
await repository.accept(command("delivery-1", issue=1))
|
||||
control_task = await repository.claim_task(QueueName.CONTROL)
|
||||
await engine_repository.accept(command("delivery-1", issue=1))
|
||||
control_task = await engine_repository.claim_task(QueueName.CONTROL)
|
||||
assert control_task is not None
|
||||
|
||||
queued_job = (await repository.accept(command("delivery-2", issue=2))).job
|
||||
await repository.apply("queued:grant", PermissionGranted(job_id=queued_job.id))
|
||||
queued_execute = await repository.claim_task(QueueName.JOBS)
|
||||
queued_job = (await engine_repository.accept(command("delivery-2", issue=2))).job
|
||||
await engine_repository.apply("queued:grant", PermissionGranted(job_id=queued_job.id))
|
||||
queued_execute = await engine_repository.claim_task(QueueName.JOBS)
|
||||
assert queued_execute is not None
|
||||
|
||||
running_job = (await repository.accept(command("delivery-3", issue=3))).job
|
||||
await repository.apply("running:grant", PermissionGranted(job_id=running_job.id))
|
||||
running_execute = await repository.claim_task(QueueName.JOBS)
|
||||
running_job = (await engine_repository.accept(command("delivery-3", issue=3))).job
|
||||
await engine_repository.apply("running:grant", PermissionGranted(job_id=running_job.id))
|
||||
running_execute = await engine_repository.claim_task(QueueName.JOBS)
|
||||
assert running_execute is not None
|
||||
await repository.apply("running:start", JobStarted(job_id=running_job.id))
|
||||
await engine_repository.apply("running:start", JobStarted(job_id=running_job.id))
|
||||
|
||||
clock["now"] = "2026-02-01T01:00:00+00:00"
|
||||
await repository.recover_tasks()
|
||||
sqlite_clock.now = "2026-02-01T01:00:00+00:00"
|
||||
await engine_repository.recover_tasks()
|
||||
|
||||
assert (
|
||||
task_storage(repository, control_task.id),
|
||||
task_storage(repository, queued_execute.id),
|
||||
task_storage(repository, running_execute.id),
|
||||
[job.id for job in await repository.running_jobs()],
|
||||
task_storage(engine_repository, control_task.id),
|
||||
task_storage(engine_repository, queued_execute.id),
|
||||
task_storage(engine_repository, running_execute.id),
|
||||
[job.id for job in await engine_repository.running_jobs()],
|
||||
) == (
|
||||
(
|
||||
"pending",
|
||||
|
||||
Reference in New Issue
Block a user