Files
agentci/tests/test_repository_migration.py
T
StanPonomarev ce9f1e3d20
Publish container image / Build and push (push) Successful in 32s
refactor tests
2026-07-26 23:49:40 +02:00

416 lines
14 KiB
Python

import sqlite3
from contextlib import closing
from pathlib import Path
import pytest
from agentci.engine.model import JobKind, JobStatus, Workflow, WorkflowKind, WorkflowStatus
from agentci.engine.repository import Repository
MIGRATIONS = Path(__file__).parents[1] / "src" / "agentci" / "migrations"
PRE_V3_CREATED_AT = "2026-01-01T00:00:00+00:00"
def create_schema_v2_database(database_path: Path, workspace: Path) -> None:
with closing(sqlite3.connect(database_path)) as connection, connection:
connection.executescript((MIGRATIONS / "001_initial.sql").read_text())
connection.execute(
"INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)",
(1, "2025-11-01T00:00:00+00:00"),
)
connection.executescript((MIGRATIONS / "002_opencode_sessions.sql").read_text())
connection.execute(
"INSERT INTO schema_migrations(version, applied_at) VALUES (?, ?)",
(2, "2025-12-01T00:00:00+00:00"),
)
connection.executemany(
"INSERT INTO deliveries(delivery_id, comment_id, received_at) VALUES (?, ?, ?)",
[
("delivery-queued", 102, "2025-12-31T23:59:58+00:00"),
("delivery-unrelated", 999, "2025-12-31T23:59:59+00:00"),
],
)
connection.executemany(
"""INSERT INTO workflows(
id, kind, repo_owner, repo_name, issue_number, pr_number, base_sha, branch,
workspace_path, primary_session_id, reviewer_session_id, artifact, review_json,
status, created_at, updated_at, runtime
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
(
"pre-v3-plan-workflow",
"plan",
"alice",
"repo",
7,
None,
"plan-base",
"agent/plan",
str(workspace / "plan"),
"plan-primary",
"plan-reviewer",
"# Existing plan",
'{"verdict":"approved"}',
"active",
"2025-12-30T00:00:00+00:00",
"2025-12-31T00:00:00+00:00",
"opencode",
),
(
"pre-v3-implementation-workflow",
"implement",
"alice",
"repo",
8,
44,
"implementation-base",
"agent/implementation",
str(workspace / "implementation"),
"implementation-primary",
"implementation-reviewer",
"# Existing implementation",
'{"verdict":"changes_requested"}',
"completed",
"2025-12-29T00:00:00+00:00",
"2025-12-31T12:00:00+00:00",
"codex",
),
],
)
connection.executemany(
"""INSERT INTO jobs(
id, kind, target_key, repo_owner, repo_name, issue_number, pr_number,
requester, message, comment_id, workflow_id, status, stage, error,
accepted_comment_id, started_comment_id, created_at, started_at, finished_at,
runtime_session_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
(
"pre-v3-job-a-terminal",
"iterate_implement",
"alice/repo:pr:44",
"alice",
"repo",
8,
44,
"bob",
"address review",
101,
"pre-v3-implementation-workflow",
"succeeded",
"completed",
None,
201,
202,
PRE_V3_CREATED_AT,
"2026-01-01T00:01:00+00:00",
"2026-01-01T00:02:00+00:00",
"implementation-runtime",
),
(
"pre-v3-job-b-queued",
"iterate_plan",
"alice/repo:issue:7",
"alice",
"repo",
7,
None,
"alice",
"refine plan",
102,
"pre-v3-plan-workflow",
"queued",
"queued",
None,
203,
204,
PRE_V3_CREATED_AT,
None,
None,
"plan-runtime",
),
],
)
@pytest.fixture
async def migrated_pre_v3_repository(tmp_path: Path) -> Repository:
database_path = tmp_path / "schema-v2.sqlite3"
create_schema_v2_database(database_path, tmp_path / "workspaces")
repository = Repository(database_path, MIGRATIONS)
await repository.initialize()
return repository
async def test_schema_v2_migration_preserves_jobs_and_reconstructs_state_machine_identity(
migrated_pre_v3_repository: Repository,
) -> None:
terminal = await migrated_pre_v3_repository.get_job("pre-v3-job-a-terminal")
queued = await migrated_pre_v3_repository.get_job("pre-v3-job-b-queued")
with (
closing(sqlite3.connect(migrated_pre_v3_repository.database_path)) as connection,
connection,
):
migrated = connection.execute(
"SELECT id, delivery_id, receive_sequence, command_body FROM jobs "
"ORDER BY receive_sequence"
).fetchall()
storage_fields = connection.execute(
"""SELECT id, started_comment_id, created_at, started_at, finished_at
FROM jobs ORDER BY receive_sequence"""
).fetchall()
assert migrated == [
(
"pre-v3-job-a-terminal",
"legacy:pre-v3-job-a-terminal",
1,
"/agent iterate address review",
),
("pre-v3-job-b-queued", "delivery-queued", 2, "/agent iterate refine plan"),
]
assert (
terminal
and (
terminal.kind,
terminal.target_key,
terminal.repo_owner,
terminal.repo_name,
terminal.issue_number,
terminal.pr_number,
terminal.requester,
terminal.message,
terminal.comment_id,
terminal.workflow_id,
terminal.status,
terminal.stage,
terminal.error,
terminal.runtime_session_id,
terminal.accepted_comment_id,
terminal.comment_body,
),
queued
and (
queued.kind,
queued.target_key,
queued.requester,
queued.message,
queued.comment_id,
queued.workflow_id,
queued.status,
queued.stage,
queued.runtime_session_id,
queued.accepted_comment_id,
),
storage_fields,
) == (
(
JobKind.ITERATE_IMPLEMENT,
"alice/repo:pr:44",
"alice",
"repo",
8,
44,
"bob",
"address review",
101,
"pre-v3-implementation-workflow",
JobStatus.SUCCEEDED,
"completed",
None,
"implementation-runtime",
201,
None,
),
(
JobKind.ITERATE_PLAN,
"alice/repo:issue:7",
"alice",
"refine plan",
102,
"pre-v3-plan-workflow",
JobStatus.QUEUED,
"queued",
"plan-runtime",
203,
),
[
(
"pre-v3-job-a-terminal",
202,
PRE_V3_CREATED_AT,
"2026-01-01T00:01:00+00:00",
"2026-01-01T00:02:00+00:00",
),
("pre-v3-job-b-queued", 204, PRE_V3_CREATED_AT, None, None),
],
)
async def test_schema_v2_migration_preserves_pre_v3_workflows(
migrated_pre_v3_repository: Repository,
tmp_path: Path,
) -> None:
plan = await migrated_pre_v3_repository.get_workflow("pre-v3-plan-workflow")
implementation = await migrated_pre_v3_repository.get_workflow("pre-v3-implementation-workflow")
with (
closing(sqlite3.connect(migrated_pre_v3_repository.database_path)) as connection,
connection,
):
timestamps = connection.execute(
"SELECT id, created_at, updated_at FROM workflows ORDER BY id"
).fetchall()
assert ((plan, implementation), timestamps) == (
(
Workflow(
id="pre-v3-plan-workflow",
kind=WorkflowKind.PLAN,
repo_owner="alice",
repo_name="repo",
issue_number=7,
workspace_path=tmp_path / "workspaces" / "plan",
base_sha="plan-base",
runtime="opencode",
branch="agent/plan",
primary_session_id="plan-primary",
reviewer_session_id="plan-reviewer",
artifact="# Existing plan",
review_json='{"verdict":"approved"}',
status=WorkflowStatus.ACTIVE,
),
Workflow(
id="pre-v3-implementation-workflow",
kind=WorkflowKind.IMPLEMENT,
repo_owner="alice",
repo_name="repo",
issue_number=8,
pr_number=44,
workspace_path=tmp_path / "workspaces" / "implementation",
base_sha="implementation-base",
runtime="codex",
branch="agent/implementation",
primary_session_id="implementation-primary",
reviewer_session_id="implementation-reviewer",
artifact="# Existing implementation",
review_json='{"verdict":"changes_requested"}',
status=WorkflowStatus.COMPLETED,
),
),
[
(
"pre-v3-implementation-workflow",
"2025-12-29T00:00:00+00:00",
"2025-12-31T12:00:00+00:00",
),
(
"pre-v3-plan-workflow",
"2025-12-30T00:00:00+00:00",
"2025-12-31T00:00:00+00:00",
),
],
)
async def test_schema_v2_migration_creates_audit_events_and_only_queued_tasks(
migrated_pre_v3_repository: Repository,
) -> None:
with (
closing(sqlite3.connect(migrated_pre_v3_repository.database_path)) as connection,
connection,
):
events = connection.execute(
"""SELECT event_id, job_id, event_type, payload_json, created_at
FROM job_events ORDER BY job_id"""
).fetchall()
tasks = connection.execute(
"""SELECT job_id, source_event_id, ordinal, listener, queue, status,
available_at, created_at
FROM listener_tasks ORDER BY ordinal"""
).fetchall()
assert (events, tasks) == (
[
(
"delivery:legacy:pre-v3-job-a-terminal",
"pre-v3-job-a-terminal",
"legacy",
"{}",
PRE_V3_CREATED_AT,
),
(
"delivery:delivery-queued",
"pre-v3-job-b-queued",
"legacy",
"{}",
PRE_V3_CREATED_AT,
),
],
[
(
"pre-v3-job-b-queued",
"delivery:delivery-queued",
0,
"execute",
"jobs",
"pending",
PRE_V3_CREATED_AT,
PRE_V3_CREATED_AT,
),
(
"pre-v3-job-b-queued",
"delivery:delivery-queued",
1,
"reconcile_comment",
"control",
"pending",
PRE_V3_CREATED_AT,
PRE_V3_CREATED_AT,
),
],
)
def durable_snapshot(database_path: Path) -> tuple[object, ...]:
with closing(sqlite3.connect(database_path)) as connection, connection:
return (
connection.execute("SELECT version FROM schema_migrations ORDER BY version").fetchall(),
connection.execute(
"""SELECT id, kind, target_key, repo_owner, repo_name, issue_number, pr_number,
requester, message, comment_id, delivery_id, receive_sequence,
command_body, workflow_id, status, stage, error, runtime_session_id,
accepted_comment_id, started_comment_id, comment_body, created_at,
started_at, finished_at
FROM jobs ORDER BY id"""
).fetchall(),
connection.execute(
"""SELECT id, kind, repo_owner, repo_name, issue_number, pr_number, base_sha,
branch, workspace_path, primary_session_id, reviewer_session_id,
artifact, review_json, status, runtime, created_at, updated_at
FROM workflows ORDER BY id"""
).fetchall(),
connection.execute(
"""SELECT event_id, job_id, event_type, payload_json
FROM job_events ORDER BY event_id"""
).fetchall(),
connection.execute(
"""SELECT job_id, source_event_id, ordinal, listener, queue, status
FROM listener_tasks ORDER BY id"""
).fetchall(),
)
async def test_reopening_migrated_schema_v2_database_is_idempotent(
migrated_pre_v3_repository: Repository,
) -> None:
before = durable_snapshot(migrated_pre_v3_repository.database_path)
reopened = Repository(migrated_pre_v3_repository.database_path, MIGRATIONS)
await reopened.initialize()
assert (before[0], durable_snapshot(reopened.database_path)) == (
[(1,), (2,), (3,)],
before,
)