Level A — merge/deploy serialization within one repo: reuse the existing ORCH-043/065 merge-lease (no new mechanism); the only new logic is an unconditional pre-merge rebase in check_branch_mergeable — under the held lease, auto_rebase_onto_main is ALWAYS called when premerge_rebase_always (default True), not just when the branch is behind. No-op on an up-to-date branch (rebase keeps HEAD, force-with-lease -> "Everything up-to-date", CI not triggered). Kill-switch off -> ORCH-043 behaviour 1:1. Level B — declarative task dependencies: additive job_deps table (CREATE ... IF NOT EXISTS, no live-DB migration); claim_next_job gate (NOT EXISTS) defers a job whose depends-on tasks are not yet 'done' without occupying a max_concurrency slot; inert on empty job_deps -> zero regression. New leaf src/task_deps.py (never-raise): is_task_ready (fail-open), DFS cycle detection + Blocked/alert, declare/ingest_plane_relations (db source never hits the network on the hot path), snapshot. Telegram waiting-line, /queue observability, reconciler skip + cycle backstop, reaper untouched. Invariants unchanged: STAGE_TRANSITIONS, QG_CHECKS registry (dep gate is a claim_next_job врезка, not a registered QG), DB schema of existing tables, HTTP endpoints; non-self repos remain a no-op on empty deps/scope. Flags: ORCH_PREMERGE_REBASE_ALWAYS, ORCH_TASK_DEPS_ENABLED, ORCH_TASK_DEPS_SOURCE. Docs: docs/architecture/README.md, CLAUDE.md, .env.example, CHANGELOG.md, adr-0015. Tests: tests/test_orch026_*.py (64 tests); full suite 991 green. Refs: ORCH-026 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""ORCH-026 — additive job_deps migration (TC-G01, AC-G4).
|
|
|
|
The migration must be additive (CREATE TABLE/INDEX IF NOT EXISTS), idempotent,
|
|
and safe on a pre-existing DB with data: existing columns of jobs/tasks/
|
|
agent_runs/events are untouched.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_orch026_migration.db")
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
import src.db as db # noqa: E402
|
|
from src.db import init_db, get_db # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def dbfile(tmp_path, monkeypatch):
|
|
f = tmp_path / "mig.db"
|
|
monkeypatch.setattr(db.settings, "db_path", str(f))
|
|
return f
|
|
|
|
|
|
def _columns(conn, table):
|
|
return [r[1] for r in conn.execute(f"PRAGMA table_info({table})").fetchall()]
|
|
|
|
|
|
def test_job_deps_table_created(dbfile):
|
|
init_db()
|
|
conn = get_db()
|
|
cols = _columns(conn, "job_deps")
|
|
conn.close()
|
|
assert set(cols) == {"task_id", "depends_on_task_id", "created_at"}
|
|
|
|
|
|
def test_job_deps_indices_created(dbfile):
|
|
init_db()
|
|
conn = get_db()
|
|
idx = {r[1] for r in conn.execute("PRAGMA index_list(job_deps)").fetchall()}
|
|
conn.close()
|
|
assert "idx_job_deps_task" in idx
|
|
assert "idx_job_deps_depends" in idx
|
|
|
|
|
|
def test_primary_key_idempotent_insert(dbfile):
|
|
init_db()
|
|
conn = get_db()
|
|
conn.execute("INSERT OR IGNORE INTO job_deps (task_id, depends_on_task_id) VALUES (1, 2)")
|
|
conn.execute("INSERT OR IGNORE INTO job_deps (task_id, depends_on_task_id) VALUES (1, 2)")
|
|
conn.commit()
|
|
n = conn.execute("SELECT COUNT(*) FROM job_deps").fetchone()[0]
|
|
conn.close()
|
|
assert n == 1, "PK (task_id, depends_on_task_id) prevents dup rows"
|
|
|
|
|
|
def test_migration_idempotent_and_preserves_data(dbfile):
|
|
# First init + seed legacy data.
|
|
init_db()
|
|
conn = get_db()
|
|
conn.execute(
|
|
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) "
|
|
"VALUES ('ET-1','ET-1','enduro-trails','feature/x','development')"
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO jobs (agent, repo, status) VALUES ('developer','enduro-trails','queued')"
|
|
)
|
|
conn.commit()
|
|
tasks_cols_before = _columns(conn, "tasks")
|
|
jobs_cols_before = _columns(conn, "jobs")
|
|
conn.close()
|
|
|
|
# Re-run init_db (simulates a restart on a live DB) -> must be a no-op.
|
|
init_db()
|
|
conn = get_db()
|
|
assert _columns(conn, "tasks") == tasks_cols_before, "tasks columns unchanged"
|
|
assert _columns(conn, "jobs") == jobs_cols_before, "jobs columns unchanged"
|
|
# Legacy data survives.
|
|
assert conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 1
|
|
assert conn.execute("SELECT COUNT(*) FROM jobs").fetchone()[0] == 1
|
|
conn.close()
|