"""ORCH-071 — post-deploy merge verification + rollout conditionality. Covers TC-01..04 (FR-2/G1/AC-1/AC-7: verify_merged_to_main), TC-11 (AC-4b: non-self repo no-op) and TC-12 (AC-10: kill-switch). All deterministic: git/HTTP are mocked, the verifier honours the never-raise contract. """ import pytest from src import merge_gate class _R: """Minimal stand-in for a completed subprocess result (returncode only).""" def __init__(self, rc): self.returncode = rc self.stdout = "" self.stderr = "" @pytest.fixture(autouse=True) def _enable(monkeypatch): # The conftest disables the under-gate by default; these tests target it, so # re-enable the feature and pin the scope to self-hosting only (empty CSV). monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", True) monkeypatch.setattr(merge_gate.settings, "merge_verify_repos", "") monkeypatch.setattr(merge_gate.settings, "merge_verify_timeout_s", 5) # --------------------------------------------------------------------------- # TC-01: validated SHA is an ancestor of origin/main (merge-base rc=0) -> True. # --------------------------------------------------------------------------- def test_tc01_verify_true_when_sha_is_ancestor(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt") calls = [] def fake_run(cmd, *a, **k): calls.append(cmd) # fetch -> rc 0; merge-base --is-ancestor -> rc 0 (is ancestor). return _R(0) monkeypatch.setattr(merge_gate.subprocess, "run", fake_run) assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "abc123") is True # The verifier consulted git merge-base --is-ancestor on origin/main. assert any("merge-base" in c and "--is-ancestor" in c and "origin/main" in c for c in calls) # --------------------------------------------------------------------------- # TC-02 (ORCH-073 FR-1): PR.merged==true NO LONGER confirms a merge. The former # OR-branch was the phantom-merge root cause (a merged docs-PR turned verify green). # SHA-in-main is now the SINGLE criterion; an empty SHA -> inconclusive -> False. # --------------------------------------------------------------------------- def test_tc02_pr_merged_does_not_confirm_without_sha_in_main(monkeypatch): # Even if a (docs-)PR is reported merged, that must NOT short-circuit to True. monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: True) monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt") # SHA not an ancestor of origin/main (rc=1) -> not confirmed despite merged PR. monkeypatch.setattr( merge_gate.subprocess, "run", lambda cmd, *a, **k: _R(1) if "merge-base" in cmd else _R(0), ) assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "abc123") is False # And an empty SHA is inconclusive -> False (cannot prove SHA-in-main). assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "") is False # --------------------------------------------------------------------------- # TC-03: not an ancestor (rc=1) AND PR not merged -> False (phantom merge). # --------------------------------------------------------------------------- def test_tc03_verify_false_when_phantom(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt") def fake_run(cmd, *a, **k): if "merge-base" in cmd: return _R(1) # NOT an ancestor. return _R(0) # fetch ok. monkeypatch.setattr(merge_gate.subprocess, "run", fake_run) assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "abc123") is False # --------------------------------------------------------------------------- # TC-04 (AC-7): never-raise — a git/OS error -> False, exception not propagated. # --------------------------------------------------------------------------- def test_tc04_verify_never_raises_on_git_error(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt") def boom(*a, **k): raise OSError("git exploded") monkeypatch.setattr(merge_gate.subprocess, "run", boom) # No exception escapes; the conservative verdict is "not confirmed". assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "abc123") is False def test_tc04_verify_never_raises_on_worktree_error(monkeypatch): # ORCH-073: verify no longer consults pr_already_merged; a worktree/git error # on the SHA-in-main path is the failure to swallow -> conservative False. def boom(*a, **k): raise RuntimeError("worktree exploded") monkeypatch.setattr(merge_gate, "ensure_worktree", boom) assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "abc123") is False # --------------------------------------------------------------------------- # TC-11 (AC-4b): non-self repo -> under-gate is a no-op (merge stays with deployer). # --------------------------------------------------------------------------- def test_tc11_non_self_repo_does_not_apply(monkeypatch): # Empty CSV -> only the self-hosting repo is in scope. assert merge_gate.merge_verify_applies("orchestrator") is True assert merge_gate.merge_verify_applies("enduro-trails") is False def test_tc11_csv_scopes_to_listed_repos(monkeypatch): monkeypatch.setattr(merge_gate.settings, "merge_verify_repos", "enduro-trails") assert merge_gate.merge_verify_applies("enduro-trails") is True # When the CSV is set, the self repo is NOT auto-included. assert merge_gate.merge_verify_applies("orchestrator") is False # --------------------------------------------------------------------------- # TC-12 (AC-10): kill-switch off -> applies False for everyone (1:1 prior behaviour). # --------------------------------------------------------------------------- def test_tc12_kill_switch_disables_under_gate(monkeypatch): monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", False) assert merge_gate.merge_verify_applies("orchestrator") is False assert merge_gate.merge_verify_applies("enduro-trails") is False