"""ORCH-071 — deterministic merge-actor (merge_gate.merge_pr). Covers TC-07 (FR-1: merge via Gitea PR-merge API, no push/force-push), TC-08 (AC-9: idempotency — already-merged -> no-op), TC-09 (AC-7: never-raise) and TC-13 (AC-8/INV-2: self-hosting safety — no prod restart, no direct/force push to main). Gitea HTTP is mocked; the actor must NEVER shell out to git/docker/ssh. """ import httpx import pytest from src import merge_gate class _Resp: def __init__(self, status_code, payload=None, text=""): self.status_code = status_code self._payload = payload if payload is not None else [] self.text = text def json(self): return self._payload @pytest.fixture(autouse=True) def _settings(monkeypatch): monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", True) monkeypatch.setattr(merge_gate.settings, "gitea_url", "http://gitea.test") monkeypatch.setattr(merge_gate.settings, "gitea_owner", "admin") monkeypatch.setattr(merge_gate.settings, "gitea_token", "tok") monkeypatch.setattr(merge_gate.settings, "merge_pr_timeout_s", 5) # --------------------------------------------------------------------------- # TC-07: an OPEN PR -> the actor calls Gitea POST /pulls/{index}/merge (Do: merge). # --------------------------------------------------------------------------- def test_tc07_merge_actor_calls_gitea_merge(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) branch = "feature/ORCH-071-x" get_calls, post_calls = [], [] def fake_get(url, params=None, headers=None, timeout=None): get_calls.append((url, params)) return _Resp(200, [{"head": {"ref": branch}, "base": {"ref": "main"}, "number": 7}]) def fake_post(url, json=None, headers=None, timeout=None): post_calls.append((url, json)) return _Resp(200) monkeypatch.setattr(httpx, "get", fake_get) monkeypatch.setattr(httpx, "post", fake_post) ok, msg = merge_gate.merge_pr("orchestrator", branch) assert ok is True assert "PR #7" in msg # POST hit the PR-merge API endpoint with Do=merge. assert len(post_calls) == 1 url, body = post_calls[0] assert url.endswith("/repos/admin/orchestrator/pulls/7/merge") assert body == {"Do": "merge"} # --------------------------------------------------------------------------- # TC-08 (AC-9): already-merged PR -> no-op (no second merge, no Gitea error). # --------------------------------------------------------------------------- def test_tc08_idempotent_already_merged(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: True) def must_not_call(*a, **k): raise AssertionError("no Gitea call must be made when already merged") monkeypatch.setattr(httpx, "get", must_not_call) monkeypatch.setattr(httpx, "post", must_not_call) ok, msg = merge_gate.merge_pr("orchestrator", "feature/ORCH-071-x") assert ok is True assert msg == "already-merged" def test_tc08_no_open_pr_is_not_an_error(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr(httpx, "get", lambda *a, **k: _Resp(200, [])) ok, msg = merge_gate.merge_pr("orchestrator", "feature/ORCH-071-x") assert ok is False assert msg == "no open PR" # --------------------------------------------------------------------------- # TC-09 (AC-7): a Gitea HTTP error -> (False, reason), exception not propagated. # --------------------------------------------------------------------------- def test_tc09_never_raise_on_http_error(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) def boom(*a, **k): raise httpx.ConnectError("gitea unreachable") monkeypatch.setattr(httpx, "get", boom) ok, msg = merge_gate.merge_pr("orchestrator", "feature/ORCH-071-x") assert ok is False assert "merge error" in msg def test_tc09_merge_endpoint_non_2xx_is_false(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr( httpx, "get", lambda *a, **k: _Resp(200, [{"head": {"ref": "feature/ORCH-071-x"}, "base": {"ref": "main"}, "number": 3}]) ) monkeypatch.setattr(httpx, "post", lambda *a, **k: _Resp(409, text="conflict")) ok, msg = merge_gate.merge_pr("orchestrator", "feature/ORCH-071-x") assert ok is False assert "HTTP 409" in msg # --------------------------------------------------------------------------- # TC-13 (AC-8/INV-2): the merge-actor NEVER shells out (no git push/force-push, # no docker/ssh prod restart) — the only side effect is the Gitea PR-merge API. # --------------------------------------------------------------------------- def test_tc13_no_shell_out_no_force_push(monkeypatch): monkeypatch.setattr(merge_gate, "pr_already_merged", lambda r, b: False) monkeypatch.setattr( httpx, "get", lambda *a, **k: _Resp(200, [{"head": {"ref": "feature/ORCH-071-x"}, "base": {"ref": "main"}, "number": 9}]) ) monkeypatch.setattr(httpx, "post", lambda *a, **k: _Resp(200)) subprocess_calls = [] monkeypatch.setattr( merge_gate.subprocess, "run", lambda cmd, *a, **k: subprocess_calls.append(cmd), ) ok, _ = merge_gate.merge_pr("orchestrator", "feature/ORCH-071-x") assert ok is True # No subprocess (git/docker/ssh) was invoked by the merge-actor at all. assert subprocess_calls == []