Files
orchestrator/tests/test_orch073_merge_verify.py
claude-bot aff334e82b fix(merge-gate): SHA-in-main as sole merge-verify criterion + main regression guard
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>
2026-06-08 16:30:46 +03:00

100 lines
4.0 KiB
Python

"""ORCH-073 FR-1 — verify_merged_to_main: SHA-in-main is the SINGLE criterion.
Covers TC-01..04 / AC-2 / AC-6. The former OR-branch `pr_already_merged` was the
phantom-merge root cause and is removed: a merged docs-PR must NOT confirm a merge.
git/HTTP are mocked; the verifier honours the never-raise contract (INV-1).
"""
import pytest
from src import merge_gate
class _R:
"""Minimal completed-subprocess stand-in (returncode only)."""
def __init__(self, rc):
self.returncode = rc
self.stdout = ""
self.stderr = ""
@pytest.fixture(autouse=True)
def _settings(monkeypatch):
monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", True)
monkeypatch.setattr(merge_gate.settings, "merge_verify_timeout_s", 5)
# ---------------------------------------------------------------------------
# TC-01 (AC-6): sha is an ancestor of origin/main (merge-base rc=0) -> True.
# ---------------------------------------------------------------------------
def test_tc01_true_when_sha_is_ancestor(monkeypatch):
monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt")
calls = []
def fake_run(cmd, *a, **k):
calls.append(cmd)
return _R(0) # fetch ok; merge-base --is-ancestor -> 0 (ancestor)
monkeypatch.setattr(merge_gate.subprocess, "run", fake_run)
assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-073-x", "abc123") is True
assert any(
"merge-base" in c and "--is-ancestor" in c and "origin/main" in c for c in calls
)
# ---------------------------------------------------------------------------
# TC-02 (AC-2): sha NOT in main AND a merged docs-PR exists -> False.
# This is the exact ORCH-067/069 bug: a merged docs-PR must not confirm.
# ---------------------------------------------------------------------------
def test_tc02_false_when_sha_not_in_main_even_with_merged_docs_pr(monkeypatch):
# A merged docs-PR is present (mock returns True), but it must be IGNORED.
called = {"pr": False}
def fake_pr_already_merged(r, b):
called["pr"] = True
return True
monkeypatch.setattr(merge_gate, "pr_already_merged", fake_pr_already_merged)
monkeypatch.setattr(merge_gate, "ensure_worktree", lambda r, b: "/wt")
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-073-x", "abc123") is False
# The merged-PR signal is no longer consulted by the verifier at all.
assert called["pr"] is False
# ---------------------------------------------------------------------------
# TC-03: empty sha -> inconclusive -> False (fail-closed), no git consulted.
# ---------------------------------------------------------------------------
def test_tc03_empty_sha_is_false(monkeypatch):
def boom(*a, **k):
raise AssertionError("git must NOT run for an empty SHA")
monkeypatch.setattr(merge_gate, "ensure_worktree", boom)
monkeypatch.setattr(merge_gate.subprocess, "run", boom)
assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-073-x", "") is False
# ---------------------------------------------------------------------------
# TC-04 (INV-1): a git/OS error -> False, exception never propagated.
# ---------------------------------------------------------------------------
def test_tc04_never_raises_on_git_error(monkeypatch):
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)
assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-073-x", "abc123") is False
def test_tc04_never_raises_on_worktree_error(monkeypatch):
def boom(*a, **k):
raise RuntimeError("worktree down")
monkeypatch.setattr(merge_gate, "ensure_worktree", boom)
assert merge_gate.verify_merged_to_main("orchestrator", "feature/ORCH-073-x", "abc123") is False