Files
orchestrator/tests/test_orch026_premerge_rebase.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

83 lines
3.5 KiB
Python

"""ORCH-026 Level A (TC-A01): proactive pre-merge rebase.
check_branch_mergeable must ALWAYS rebase the task branch onto the current
origin/main under the held merge-lease when ``premerge_rebase_always`` is on —
even when ``branch_is_behind_main`` would short-circuit (no conflict, formally
not behind). With the flag OFF the ORCH-043 short-circuit is restored 1:1.
These are pure unit tests: every merge_gate primitive is monkeypatched, so no
git/network is touched — we assert the CONTROL FLOW (was auto_rebase_onto_main
called?) and the verdict.
"""
import os
import tempfile
import pytest
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch026_premerge.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 patched_gate(monkeypatch):
"""Patch merge_gate primitives; record whether auto_rebase ran."""
calls = {"rebase": 0, "retest": 0, "released": 0, "behind_checked": 0}
monkeypatch.setattr(checks.settings, "merge_gate_enabled", True, raising=False)
monkeypatch.setattr(checks.settings, "merge_gate_repos", "", raising=False)
monkeypatch.setattr(merge_gate, "acquire_merge_lease",
lambda *a, **k: (True, "lease acquired"), raising=False)
def _behind(repo, branch):
calls["behind_checked"] += 1
return False # NOT behind -> ORCH-043 would short-circuit
def _rebase(repo, branch):
calls["rebase"] += 1
return True, "rebased (noop)"
def _retest(repo, branch):
calls["retest"] += 1
return True, "green"
def _release(repo, branch=None):
calls["released"] += 1
monkeypatch.setattr(merge_gate, "branch_is_behind_main", _behind, raising=False)
monkeypatch.setattr(merge_gate, "auto_rebase_onto_main", _rebase, raising=False)
monkeypatch.setattr(merge_gate, "retest_branch", _retest, raising=False)
monkeypatch.setattr(merge_gate, "release_merge_lease", _release, raising=False)
return calls
def test_always_rebases_even_when_not_behind(patched_gate, monkeypatch):
"""premerge_rebase_always=True -> auto_rebase_onto_main ALWAYS called (AC-A2)."""
monkeypatch.setattr(checks.settings, "premerge_rebase_always", True, raising=False)
ok, reason = checks.check_branch_mergeable("orchestrator", "ORCH-026", "feature/x")
assert ok is True
assert patched_gate["rebase"] == 1, "rebase must run even when not behind"
assert patched_gate["retest"] == 1, "re-test must run after the proactive rebase"
def test_flag_off_short_circuits_like_orch043(patched_gate, monkeypatch):
"""premerge_rebase_always=False -> not-behind short-circuit, no rebase (AC-A7)."""
monkeypatch.setattr(checks.settings, "premerge_rebase_always", False, raising=False)
ok, reason = checks.check_branch_mergeable("orchestrator", "ORCH-026", "feature/x")
assert ok is True
assert reason == "branch up-to-date with main"
assert patched_gate["rebase"] == 0, "must NOT rebase when not behind and flag off"
def test_disabled_gate_is_noop(monkeypatch):
"""merge_gate_enabled=False -> pass-through, no lease/rebase at all (AC-G2)."""
monkeypatch.setattr(checks.settings, "merge_gate_enabled", False, raising=False)
monkeypatch.setattr(checks.settings, "premerge_rebase_always", True, raising=False)
ok, reason = checks.check_branch_mergeable("orchestrator", "ORCH-026", "feature/x")
assert ok is True
assert "disabled" in reason