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>
66 lines
3.1 KiB
Python
66 lines
3.1 KiB
Python
"""ORCH-026 Level A — serialization integration (TC-A08).
|
|
|
|
Scenario (no network, lease + gate level): two tasks of the SAME repo race for
|
|
the merge edge. While A holds the merge-lease (the merge->main-updated window),
|
|
B's check_branch_mergeable returns "merge-lock busy" -> the engine DEFERS B (it
|
|
does NOT roll back). After A releases (A reached main / done), B acquires, is
|
|
proactively rebased onto the now-current main (carrying A's code) and merges.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch026_serint.db"))
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
from src import merge_gate # noqa: E402
|
|
from src.qg import checks # noqa: E402
|
|
|
|
|
|
@pytest.fixture
|
|
def env(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(merge_gate.settings, "repos_dir", str(tmp_path), raising=False)
|
|
monkeypatch.setattr(merge_gate.settings, "merge_lock_timeout_s", 300, raising=False)
|
|
monkeypatch.setattr(checks.settings, "merge_gate_enabled", True, raising=False)
|
|
monkeypatch.setattr(checks.settings, "merge_gate_repos", "", raising=False)
|
|
monkeypatch.setattr(checks.settings, "premerge_rebase_always", True, raising=False)
|
|
# Make the git/test primitives deterministic no-ops; A's rebase is a no-op,
|
|
# B's rebase is the real "catch up to A's code".
|
|
monkeypatch.setattr(merge_gate, "branch_is_behind_main", lambda r, b: False, raising=False)
|
|
monkeypatch.setattr(merge_gate, "auto_rebase_onto_main", lambda r, b: (True, "ok"), raising=False)
|
|
monkeypatch.setattr(merge_gate, "retest_branch", lambda r, b: (True, "green"), raising=False)
|
|
return tmp_path
|
|
|
|
|
|
def test_serialized_merge_window(env, monkeypatch):
|
|
repo = "orchestrator"
|
|
# A reaches the merge edge first: gate passes and HOLDS the lease.
|
|
okA, reasonA = checks.check_branch_mergeable(repo, "ORCH-1", "feature/A")
|
|
assert okA is True
|
|
# Lease is held by A.
|
|
assert merge_gate._read_lease(merge_gate._lease_path(repo))["branch"] == "feature/A"
|
|
|
|
# B reaches the merge edge while A still holds the window -> busy -> DEFER.
|
|
okB, reasonB = checks.check_branch_mergeable(repo, "ORCH-2", "feature/B")
|
|
assert okB is False
|
|
assert reasonB == "merge-lock busy" # NOT a rollback; engine re-queues via available_at
|
|
# B's defer must NOT have stolen / cleared A's lease.
|
|
assert merge_gate._read_lease(merge_gate._lease_path(repo))["branch"] == "feature/A"
|
|
|
|
# A completes (PR merged / deploy->done) -> lease released.
|
|
merge_gate.release_merge_lease(repo, "feature/A")
|
|
|
|
# B retries: now acquires, is proactively rebased onto current main, merges.
|
|
rebased = {"called": 0}
|
|
|
|
def _rebase(r, b):
|
|
rebased["called"] += 1
|
|
return True, "rebased onto A"
|
|
|
|
monkeypatch.setattr(merge_gate, "auto_rebase_onto_main", _rebase, raising=False)
|
|
okB2, reasonB2 = checks.check_branch_mergeable(repo, "ORCH-2", "feature/B")
|
|
assert okB2 is True
|
|
assert rebased["called"] == 1, "B must be proactively rebased onto the fresh main (A's code)"
|