rewrite phase 1
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
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_reconstructs_identity_order_and_iterate_commands(
|
||||
migrated_pre_v3_repository: Repository,
|
||||
) -> None:
|
||||
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()
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
|
||||
async def test_schema_v2_migration_preserves_pre_v3_job_fields(
|
||||
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,
|
||||
):
|
||||
storage_fields = connection.execute(
|
||||
"""SELECT id, started_comment_id, created_at, started_at, finished_at
|
||||
FROM jobs ORDER BY receive_sequence"""
|
||||
).fetchall()
|
||||
|
||||
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, delivery_id, receive_sequence, command_body FROM jobs 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,
|
||||
)
|
||||
Reference in New Issue
Block a user