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