127 lines
5.6 KiB
Python
127 lines
5.6 KiB
Python
"""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: PR.merged==true short-circuits to True even if git is unavailable.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc02_verify_true_when_pr_merged_even_without_git(monkeypatch):
|
|
monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: True)
|
|
|
|
def boom(*a, **k):
|
|
raise RuntimeError("git must NOT be consulted when PR is already merged")
|
|
|
|
monkeypatch.setattr(merge_gate, "ensure_worktree", boom)
|
|
monkeypatch.setattr(merge_gate.subprocess, "run", boom)
|
|
assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-071-x", "") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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_http_error(monkeypatch):
|
|
def boom(r, b):
|
|
raise RuntimeError("gitea down")
|
|
|
|
monkeypatch.setattr(merge_gate, "pr_already_merged", 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
|