fix(merge): wire pr_already_merged guard into deployer merge path (idempotent re-merge)
All checks were successful
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 19s

The pr_already_merged guard was defined + unit-tested but consulted by zero
production code, while ADR-001 Р-3 / README / CHANGELOG claimed the merge path
consults it before a repeat merge (reviewer P1, ORCH-065 attempt 2/3). The
actual merge actor is the LLM deployer agent (it merges the feature PR at the
start of the `deploy` stage), so on a reaper re-drive of an already-merged PR
the deployer would blindly re-merge → Gitea error → false БАГ-8 rollback; AC-11
("no second merge") was not met deterministically.

Wire the guard at the real consultation point — the deployer prompt — so it
runs merge_gate.pr_already_merged before any (re-)merge and no-ops when the PR
is already merged. check_branch_mergeable is left untouched (AC-13: check_*
behaviour unchanged; it runs on the first deploy-staging→deploy edge, not on a
deploy-stage re-drive where the second-merge risk lives).

- .openclaw/agents/deployer.md: idempotent pre-merge guard step + general rule.
- src/merge_gate.py: docstring names the deployer-prompt consultation point.
- docs/architecture/README.md, CHANGELOG.md: state the consultation point so
  golden-source matches implementation.
- tests/test_merge_gate.py: regression test asserting the deployer prompt wires
  the guard (so it can't silently become dead code again).

pytest tests/ -q: 743 passed.

Refs: ORCH-065
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:45:24 +00:00
parent 2ec4bea5bc
commit aa46e5d8b0
5 changed files with 73 additions and 3 deletions

View File

@@ -353,3 +353,32 @@ def test_tc16_pr_non_200_false(monkeypatch):
httpx, "get", lambda *a, **k: _FakeResp(500, None),
)
assert merge_gate.pr_already_merged("orchestrator", "feature/x") is False
# ---------------------------------------------------------------------------
# ORCH-065 / TC-16 (wiring): the merge path consults the guard.
#
# pr_already_merged is consulted by the actual merge actor — the deployer agent
# (webhooks/gitea.py: "the deployer merges the PR at the START of its run"). The
# `deploy` stage can be re-driven by the job-reaper, so the deployer prompt MUST
# instruct an idempotent pre-merge consult of pr_already_merged (ADR-001 Р-3 /
# README / CHANGELOG). This test fails if that wiring regresses, so the guard can
# never silently become dead code again while the docs claim it is consulted.
# ---------------------------------------------------------------------------
def test_tc16_deployer_prompt_consults_guard():
"""The deployer prompt (merge path) wires the idempotent merge guard."""
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
prompt_path = os.path.join(repo_root, ".openclaw", "agents", "deployer.md")
with open(prompt_path, "r", encoding="utf-8") as f:
prompt = f.read()
# The guard function is named and the prompt instructs consulting it BEFORE merge.
assert "pr_already_merged" in prompt, "deployer prompt must name the guard"
lowered = prompt.lower()
assert "before" in lowered and "merge" in lowered, (
"deployer prompt must instruct consulting the guard BEFORE merging"
)
# The idempotent no-op contract (already merged -> no second merge) is documented.
assert "no second merge" in lowered, (
"deployer prompt must document the already-merged no-op (AC-11)"
)