Files
orchestrator/tests/test_orch026_serialize_integration.py
claude-bot a74379f657 feat(ORCH-026): task dependencies (B waits for A) + single-repo merge serialization
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>
2026-06-08 19:17:44 +03:00

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)"