Files
orchestrator/tests/test_orch073_gitattributes.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

86 lines
3.6 KiB
Python

"""ORCH-073 FR-4 — .gitattributes: CHANGELOG.md merge=union.
Covers TC-11/TC-12 / AC-4. TC-11 asserts the repo-root .gitattributes declares the
union driver (git check-attr). TC-12 proves, in a throwaway git repo, that two
branches both editing '## [Unreleased]' merge WITHOUT a conflict and BOTH entries
survive — exactly what stops auto_rebase_onto_main from rolling a branch back.
"""
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
def _git(cwd, *args, env=None):
return subprocess.run(
["git", *args], cwd=str(cwd), capture_output=True, text=True, env=env,
)
# ---------------------------------------------------------------------------
# TC-11 (AC-4): the repo-root .gitattributes declares CHANGELOG.md merge=union.
# ---------------------------------------------------------------------------
def test_tc11_gitattributes_declares_union():
ga = REPO_ROOT / ".gitattributes"
assert ga.is_file(), ".gitattributes must exist at the repo root"
assert "CHANGELOG.md merge=union" in ga.read_text(encoding="utf-8")
r = _git(REPO_ROOT, "check-attr", "merge", "CHANGELOG.md")
assert r.returncode == 0, r.stderr
# Output form: 'CHANGELOG.md: merge: union'
assert "merge: union" in r.stdout, r.stdout
# ---------------------------------------------------------------------------
# TC-12 (AC-4): two Unreleased edits merge with no conflict; both kept.
# ---------------------------------------------------------------------------
def _init_repo(tmp_path):
env = {
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
"GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null",
"PATH": __import__("os").environ.get("PATH", ""),
"HOME": str(tmp_path),
}
repo = tmp_path / "repo"
repo.mkdir()
assert _git(repo, "init", "-b", "main", env=env).returncode == 0
(repo / ".gitattributes").write_text("CHANGELOG.md merge=union\n", encoding="utf-8")
base = (
"# Changelog\n\n## [Unreleased]\n\n### Common\n\n## [0.1.0]\n- initial\n"
)
(repo / "CHANGELOG.md").write_text(base, encoding="utf-8")
_git(repo, "add", ".", env=env)
assert _git(repo, "commit", "-m", "base", env=env).returncode == 0
return repo, env
def test_tc12_union_merge_keeps_both_entries(tmp_path):
repo, env = _init_repo(tmp_path)
# Branch A adds its Unreleased line.
_git(repo, "checkout", "-b", "task-a", env=env)
txt = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
(repo / "CHANGELOG.md").write_text(
txt.replace("### Common\n", "### Common\n- ORCH-A: feature A\n"), encoding="utf-8"
)
_git(repo, "commit", "-am", "task A changelog", env=env)
# Branch B (from main) adds a DIFFERENT Unreleased line at the same spot.
_git(repo, "checkout", "main", env=env)
_git(repo, "checkout", "-b", "task-b", env=env)
txt = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
(repo / "CHANGELOG.md").write_text(
txt.replace("### Common\n", "### Common\n- ORCH-B: feature B\n"), encoding="utf-8"
)
_git(repo, "commit", "-am", "task B changelog", env=env)
# Merge A into B — union must avoid a conflict and keep BOTH lines.
m = _git(repo, "merge", "--no-edit", "task-a", env=env)
result = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
assert m.returncode == 0, f"union merge must not conflict: {m.stdout}\n{m.stderr}"
assert "<<<<<<<" not in result and ">>>>>>>" not in result
assert "ORCH-A: feature A" in result
assert "ORCH-B: feature B" in result