Root-cause fix for main erosion (phantom merge): code of ORCH-067/069 reached `done` while absent from origin/main (only their auto docs-PRs landed). - FR-1: verify_merged_to_main confirms merge ONLY by `git merge-base --is-ancestor <validated_sha> origin/main`; the OR-branch pr_already_merged is removed (a merged PR no longer confirms). Empty SHA / git error -> False. - FR-2: pr_already_merged demoted to merge_pr idempotency-guard; counts a PR only when merged & head.ref==<branch> & base.ref=="main" (explicit in-loop filter). - FR-3: merge_pr selects the open code-PR by head==<branch> AND base==main. - FR-5: new deterministic check_main_regression in _handle_merge_verify (after confirmed SHA-in-main, before done) verifies MAIN_REGRESSION_MARKERS still in origin/main; deterministic count==0 -> alert "main regressed" + HOLD (NOT done, no rollback); git error of the grep -> fail-open. Kill-switch ORCH_REGRESSION_GUARD_ENABLED; non-self -> no-op. - FR-4: root .gitattributes `CHANGELOG.md merge=union` so Unreleased edits auto-merge on rebase without conflict (branch not rolled back). Invariants unchanged (STAGE_TRANSITIONS, QG_CHECKS, deploy-status, merge-gate, image-freshness, DB schema, external HTTP API); non-self repos no-op (INV-5); never-raise (INV-1); merge only via Gitea PR-API (INV-2). Docs: CHANGELOG, .env.example (README/ADR updated by architect). Tests: tests/test_orch073_*.py (TC-01..18); existing merge-gate tests updated for the new code-PR filter. Refs: ORCH-073 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""ORCH-073 FR-2 — pr_already_merged distinguishes code-PR from docs-PR.
|
|
|
|
Covers TC-05..07. pr_already_merged is now an idempotency-guard: it counts a PR as
|
|
"merged" ONLY when it carries the code of THIS feature-branch into main
|
|
(merged & head.ref==branch & base.ref=="main"), excluding auto docs-PRs. Gitea HTTP
|
|
is mocked; never-raise -> False (INV-1).
|
|
"""
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from src import merge_gate
|
|
|
|
BRANCH = "feature/ORCH-073-x"
|
|
|
|
|
|
class _Resp:
|
|
def __init__(self, status_code, payload=None):
|
|
self.status_code = status_code
|
|
self._payload = payload if payload is not None else []
|
|
|
|
def json(self):
|
|
return self._payload
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _settings(monkeypatch):
|
|
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")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-05: a merged docs-PR (head=docs/*, base=main) is NOT counted as code-merge.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc05_merged_docs_pr_not_counted(monkeypatch):
|
|
payload = [
|
|
{"merged": True, "head": {"ref": "docs/ORCH-073-staging-log"}, "base": {"ref": "main"}},
|
|
{"merged": False, "head": {"ref": BRANCH}, "base": {"ref": "main"}},
|
|
]
|
|
monkeypatch.setattr(httpx, "get", lambda *a, **k: _Resp(200, payload))
|
|
assert merge_gate.pr_already_merged("orchestrator", BRANCH) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-06: a merged code-PR (head==branch, base==main) IS recognised.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc06_merged_code_pr_recognised(monkeypatch):
|
|
payload = [
|
|
{"merged": True, "head": {"ref": BRANCH}, "base": {"ref": "main"}},
|
|
]
|
|
monkeypatch.setattr(httpx, "get", lambda *a, **k: _Resp(200, payload))
|
|
assert merge_gate.pr_already_merged("orchestrator", BRANCH) is True
|
|
|
|
|
|
def test_tc06_merged_code_pr_onto_non_main_base_not_counted(monkeypatch):
|
|
# Right head but a foreign base (not main) must NOT count.
|
|
payload = [
|
|
{"merged": True, "head": {"ref": BRANCH}, "base": {"ref": "develop"}},
|
|
]
|
|
monkeypatch.setattr(httpx, "get", lambda *a, **k: _Resp(200, payload))
|
|
assert merge_gate.pr_already_merged("orchestrator", BRANCH) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-07: HTTP error / non-200 -> False (never-raise, conservative).
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc07_non_200_is_false(monkeypatch):
|
|
monkeypatch.setattr(httpx, "get", lambda *a, **k: _Resp(500, []))
|
|
assert merge_gate.pr_already_merged("orchestrator", BRANCH) is False
|
|
|
|
|
|
def test_tc07_http_exception_is_false(monkeypatch):
|
|
def boom(*a, **k):
|
|
raise httpx.ConnectError("gitea unreachable")
|
|
|
|
monkeypatch.setattr(httpx, "get", boom)
|
|
assert merge_gate.pr_already_merged("orchestrator", BRANCH) is False
|