"""ORCH-073 FR-5 — main-integrity regression guard wired into _handle_merge_verify. Covers TC-13..16 / AC-3 / AC-5 / AC-6 / INV-1. Calls the under-gate handler directly with mocked merge_gate primitives + side effects (Plane/Telegram). Asserts the return contract: False == advance to `done`, True == HOLD (alert, NOT done). """ import os import tempfile os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch073_rg.db")) from unittest.mock import MagicMock # noqa: E402 import pytest # noqa: E402 from src import stage_engine, image_freshness # noqa: E402 from src.stage_engine import AdvanceResult, _handle_merge_verify # noqa: E402 REPO = "orchestrator" WI = "ORCH-073" BRANCH = "feature/ORCH-073-x" @pytest.fixture(autouse=True) def _wire(monkeypatch): # Under-gate is in scope for the self-hosting repo; guard enabled. monkeypatch.setattr(stage_engine.merge_gate, "merge_verify_applies", lambda r: True) monkeypatch.setattr(stage_engine.settings, "regression_guard_enabled", True) monkeypatch.setattr(image_freshness, "validated_revision", lambda r, b: "deadbeef") monkeypatch.setattr(stage_engine.merge_gate, "merge_pr", lambda r, b: (True, "merged PR #1")) # Silence Plane/Telegram side effects (assert on .called where relevant). for name in ("set_issue_blocked", "plane_add_comment", "send_telegram", "link_for"): monkeypatch.setattr(stage_engine, name, MagicMock()) monkeypatch.setattr( stage_engine.self_deploy, "record_merged_to_main", MagicMock(return_value=True) ) # --------------------------------------------------------------------------- # TC-13 (AC-6): SHA in main AND markers intact -> advance (return False), no alert. # --------------------------------------------------------------------------- def test_tc13_confirmed_and_intact_advances(monkeypatch): monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", lambda r, b, s: True) monkeypatch.setattr(stage_engine.merge_gate, "check_main_regression", lambda r, b: (True, "markers intact (4)")) res = AdvanceResult() intervened = _handle_merge_verify(1, REPO, WI, BRANCH, res) assert intervened is False # advance to done assert res.alerted is False assert not stage_engine.set_issue_blocked.called # --------------------------------------------------------------------------- # TC-14 (AC-3): SHA NOT in main (docs-only merge) -> HOLD + alert + Blocked. # --------------------------------------------------------------------------- def test_tc14_sha_not_in_main_holds(monkeypatch): monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", lambda r, b, s: False) # Guard must never even run when SHA is not confirmed. monkeypatch.setattr( stage_engine.merge_gate, "check_main_regression", lambda r, b: (_ for _ in ()).throw(AssertionError("guard must not run when not confirmed")), ) res = AdvanceResult() intervened = _handle_merge_verify(1, REPO, WI, BRANCH, res) assert intervened is True # HOLD assert res.advanced is False assert res.note == "merge-not-verified-hold" assert stage_engine.set_issue_blocked.called assert stage_engine.send_telegram.called # --------------------------------------------------------------------------- # TC-15 (AC-5): SHA in main BUT a marker missing -> HOLD + 'main regressed' alert. # --------------------------------------------------------------------------- def test_tc15_marker_missing_holds(monkeypatch): monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", lambda r, b, s: True) monkeypatch.setattr( stage_engine.merge_gate, "check_main_regression", lambda r, b: (False, "main regressed: ORCH-067 code missing (plane_issue_link @ src/notifications.py)"), ) res = AdvanceResult() intervened = _handle_merge_verify(1, REPO, WI, BRANCH, res) assert intervened is True # HOLD, NOT done assert res.advanced is False assert res.note == "main-regressed-hold" assert stage_engine.set_issue_blocked.called assert stage_engine.send_telegram.called # --------------------------------------------------------------------------- # TC-16 (INV-1): an internal verifier error -> HOLD + alert, no exception escapes. # --------------------------------------------------------------------------- def test_tc16_internal_error_holds_never_raises(monkeypatch): def boom(r, b, s): raise RuntimeError("verifier exploded") monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", boom) res = AdvanceResult() # Must NOT raise. intervened = _handle_merge_verify(1, REPO, WI, BRANCH, res) assert intervened is True # HOLD assert res.advanced is False assert res.alerted is True assert "merge-verify-error" in (res.note or "")