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>
94 lines
4.4 KiB
Python
94 lines
4.4 KiB
Python
"""ORCH-073 — conditionality / backward-compat (INV-5).
|
|
|
|
Covers TC-17/18 / AC-6. The whole under-gate and the regression guard are no-ops for
|
|
non-self repos and when their kill-switches are off, so enduro-trails and a disabled
|
|
self-host behave exactly as before.
|
|
"""
|
|
|
|
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_cond.db"))
|
|
|
|
from unittest.mock import MagicMock # noqa: E402
|
|
|
|
from src import merge_gate, 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"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-17 (AC-6/INV-5): non-self repo / kill-switch off -> under-gate is a no-op.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc17_merge_verify_applies_scope(monkeypatch):
|
|
monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", True)
|
|
monkeypatch.setattr(merge_gate.settings, "merge_verify_repos", "")
|
|
# Empty CSV -> only the self-hosting repo.
|
|
assert merge_gate.merge_verify_applies("orchestrator") is True
|
|
assert merge_gate.merge_verify_applies("enduro-trails") is False
|
|
# Kill-switch off -> no-op for everyone.
|
|
monkeypatch.setattr(merge_gate.settings, "merge_verify_enabled", False)
|
|
assert merge_gate.merge_verify_applies("orchestrator") is False
|
|
|
|
|
|
def test_tc17_under_gate_noop_for_non_self(monkeypatch):
|
|
# When the under-gate does not apply, _handle_merge_verify advances (False) and
|
|
# never touches the merge-actor / verifier / guard.
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_verify_applies", lambda r: False)
|
|
|
|
def must_not_call(*a, **k):
|
|
raise AssertionError("under-gate must be a no-op for non-self repos")
|
|
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_pr", must_not_call)
|
|
monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", must_not_call)
|
|
monkeypatch.setattr(stage_engine.merge_gate, "check_main_regression", must_not_call)
|
|
|
|
res = AdvanceResult()
|
|
assert _handle_merge_verify(1, "enduro-trails", WI, BRANCH, res) is False
|
|
assert res.alerted is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-18 (INV-5): regression guard respects its kill-switch -> no-op; SHA-in-main
|
|
# alone still advances the task.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc18_guard_kill_switch_skips_guard(monkeypatch):
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_verify_applies", lambda r: True)
|
|
monkeypatch.setattr(stage_engine.settings, "regression_guard_enabled", False)
|
|
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"))
|
|
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: (_ for _ in ()).throw(AssertionError("guard must not run when disabled")),
|
|
)
|
|
monkeypatch.setattr(
|
|
stage_engine.self_deploy, "record_merged_to_main", MagicMock(return_value=True)
|
|
)
|
|
for name in ("set_issue_blocked", "plane_add_comment", "send_telegram", "link_for"):
|
|
monkeypatch.setattr(stage_engine, name, MagicMock())
|
|
|
|
res = AdvanceResult()
|
|
# Guard disabled -> confirmed SHA-in-main advances straight to done (return False).
|
|
assert _handle_merge_verify(1, REPO, WI, BRANCH, res) is False
|
|
assert res.alerted is False
|
|
assert not stage_engine.set_issue_blocked.called
|
|
|
|
|
|
def test_tc18_guard_noop_for_non_self_repo(monkeypatch):
|
|
# check_main_regression is only invoked inside the confirmed branch which itself
|
|
# only runs when merge_verify_applies is True (self-hosting / CSV). For a non-self
|
|
# repo the guard is never reached.
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_verify_applies", lambda r: False)
|
|
monkeypatch.setattr(
|
|
stage_engine.merge_gate, "check_main_regression",
|
|
lambda r, b: (_ for _ in ()).throw(AssertionError("guard must not run for non-self")),
|
|
)
|
|
res = AdvanceResult()
|
|
assert _handle_merge_verify(1, "enduro-trails", WI, BRANCH, res) is False
|