"""anu_v2.tests.test_replacement_pr_runner_2537 — 회귀 7건 (회장 §명시 1:1).

회귀 케이스 (회장 §명시):
  1. clean PR contamination 0       — expected_files == diff → contaminated=False
  2. extra files contamination       — expected_files 외 파일 존재 → contaminated=True
  3. missing files contamination     — expected_files 일부 부재 → contaminated=True
  4. original PR 보존                — preserve 후 close/abort gh 호출 0 + OPEN 유지
  5. clean replacement 생성          — clean branch + 새 PR + queue ready
  6. replacement 실패 → Critical 7종 — 단계별 classify_failure 매핑 정확
  7. executor 인터페이스 contract    — replacement_pr_required 키 제공

본 회귀는 anu_v2/* 모듈만 import 한다 (one-way isolation).
"""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path
from typing import Any, Mapping, Sequence

import pytest

# workspace root → sys.path (anu_v2 패키지를 절대 import 하기 위함)
WORKSPACE_ROOT = Path(__file__).resolve().parents[2]
if str(WORKSPACE_ROOT) not in sys.path:
    sys.path.insert(0, str(WORKSPACE_ROOT))

from anu_v2.merge_queue_executor import (  # noqa: E402
    CRITICAL_CODES,
    CRITICAL_DIFF_REPLACEMENT_FAILED,
    CRITICAL_REPLACEMENT_FAILED,
)
from anu_v2.replacement_pr_runner import (  # noqa: E402
    ContaminationReport,
    PreservationRecord,
    ReplacementFailure,
    ReplacementPRRunner,
    ReplacementResult,
)


# ─── helpers ────────────────────────────────────────────────────────────────
def _cp(returncode: int = 0, stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:
    return subprocess.CompletedProcess(args=[], returncode=returncode, stdout=stdout, stderr=stderr)


class _GitRecorder:
    """git_runner mock — 각 호출의 인자/env 를 기록하고 시나리오별 응답 반환."""

    def __init__(self, scenarios: list[subprocess.CompletedProcess] | None = None) -> None:
        self.calls: list[tuple[tuple[str, ...], dict[str, str]]] = []
        self._scenarios = list(scenarios) if scenarios else []

    def __call__(
        self,
        args: Sequence[str],
        env: Mapping[str, str] | None,
    ) -> subprocess.CompletedProcess:
        self.calls.append((tuple(args), dict(env or {})))
        if self._scenarios:
            return self._scenarios.pop(0)
        return _cp()


class _GhRecorder(_GitRecorder):
    """gh_runner mock — git 과 동일 패턴, 별도 클래스로 명시."""


class _AuditRecorder:
    def __init__(self) -> None:
        self.records: list[dict[str, Any]] = []

    def __call__(self, record: Mapping[str, Any]) -> None:
        self.records.append(dict(record))


def _make_runner(
    *,
    git_runner: _GitRecorder | None = None,
    gh_runner: _GhRecorder | None = None,
    audit_writer: _AuditRecorder | None = None,
    audit_root: Path | None = None,
    bot_token_env: str = "BOT_GITHUB_TOKEN",
) -> tuple[ReplacementPRRunner, _GitRecorder, _GhRecorder, _AuditRecorder]:
    git = git_runner or _GitRecorder()
    gh = gh_runner or _GhRecorder()
    audit = audit_writer or _AuditRecorder()
    root = audit_root or Path("/tmp/anu_v2_audit_test")
    runner = ReplacementPRRunner(
        gh_runner=gh,
        git_runner=git,
        audit_writer=audit,
        audit_root=root,
        bot_token_env=bot_token_env,
    )
    return runner, git, gh, audit


EXPECTED_FILES = (
    "anu_v2/replacement_pr_runner.py",
    "anu_v2/tests/test_replacement_pr_runner_2537.py",
)


# ── 회귀 1. clean PR contamination 0 ─────────────────────────────────────────
def test_detect_contamination_clean_when_files_match() -> None:
    runner, _, _, _ = _make_runner()
    report = runner.detect_contamination(
        original_pr_diff=list(EXPECTED_FILES),
        expected_files=list(EXPECTED_FILES),
    )
    assert isinstance(report, ContaminationReport)
    assert report.contaminated is False
    assert report.extra_files == ()
    assert report.missing_files == ()


# ── 회귀 2. extra files contamination ────────────────────────────────────────
def test_detect_contamination_extra_files_flagged() -> None:
    runner, _, _, _ = _make_runner()
    report = runner.detect_contamination(
        original_pr_diff=list(EXPECTED_FILES) + [
            # 회장 §금지 — anu_v2 외 path 가 PR diff 에 들어오면 즉시 contamination.
            "scripts/dispatch.py",
            "utils/some_helper.py",
        ],
        expected_files=list(EXPECTED_FILES),
    )
    assert report.contaminated is True
    assert report.extra_files == ("scripts/dispatch.py", "utils/some_helper.py")
    assert report.missing_files == ()


# ── 회귀 3. missing files contamination ──────────────────────────────────────
def test_detect_contamination_missing_files_flagged() -> None:
    runner, _, _, _ = _make_runner()
    report = runner.detect_contamination(
        original_pr_diff=[EXPECTED_FILES[0]],          # 본체만 들어오고 테스트 누락
        expected_files=list(EXPECTED_FILES),
    )
    assert report.contaminated is True
    assert report.extra_files == ()
    assert report.missing_files == (EXPECTED_FILES[1],)


# ── 회귀 4. original PR 보존 ─────────────────────────────────────────────────
def test_preserve_original_pr_open_state_no_close_or_abort(tmp_path: Path) -> None:
    runner, git, gh, audit = _make_runner(audit_root=tmp_path)
    record = runner.preserve_original_pr(pr_number=84)
    assert isinstance(record, PreservationRecord)
    assert record.original_pr == 84
    assert record.preserved_state == "OPEN"          # close/abort 미수행
    # gh / git 호출 0 — preserve 메서드는 PR 상태를 일체 변경하지 않는다
    assert git.calls == []
    assert gh.calls == []
    # audit 박제 1건 (OPEN 유지 명시)
    assert len(audit.records) == 1
    assert audit.records[0]["pr"] == 84
    assert audit.records[0]["decision"] == "ORIGINAL_PR_PRESERVED"
    assert audit.records[0]["note"] == "OPEN 유지 — close/abort 미수행"
    # audit 파일 경로가 audit_root 내부로 한정되는지 확인
    assert record.audit_path.startswith(str(tmp_path))


# ── 회귀 5. clean replacement 생성 ──────────────────────────────────────────
def test_create_clean_replacement_success(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")

    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(),                                                  # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                                  # checkout PR head -- (일괄 stage: 모든 expected_files)
        _cp(),                                                  # commit
        _cp(),                                                  # push
        _cp(stdout="abc1234deadbeef\n"),                        # rev-parse HEAD
    ]
    gh_scenarios = [
        _cp(stdout="https://github.com/owner/repo/pull/91\n"),  # pr create
    ]
    git = _GitRecorder(git_scenarios)
    gh = _GhRecorder(gh_scenarios)
    runner, _, _, audit = _make_runner(git_runner=git, gh_runner=gh, audit_root=tmp_path)

    result = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(result, ReplacementResult)
    assert result.replacement_pr == 91
    assert result.clean_sha == "abc1234deadbeef"
    assert result.clean_branch == "task/task-2537-dev4-clean"
    assert result.merge_queue_ready is True

    # BOT 토큰 process-local injection 검증 (모든 git/gh 호출의 env)
    for _, env in git.calls + gh.calls:
        assert env.get("GH_TOKEN") == "bot-test-token"
        assert env.get("GITHUB_TOKEN") == "bot-test-token"

    # 금지 플래그 (admin/force/rebase) 누설 0 — assert_no_forbidden_git_flags 가 호출 측에서 정적 차단
    for args, _ in git.calls + gh.calls:
        joined = " ".join(args)
        assert "--admin" not in joined
        assert "--force" not in joined
        assert "--rebase" not in joined

    # original PR 상태 변경 시도 0 (close / merge / edit 호출 없음)
    for args, _ in gh.calls:
        head = args[0] if args else ""
        action = args[1] if len(args) > 1 else ""
        if head == "pr":
            assert action == "create"

    # audit 박제 1건 (replacement 성공)
    success_records = [r for r in audit.records if r.get("decision") == "REPLACEMENT_PR_CREATED"]
    assert len(success_records) == 1
    assert success_records[0]["original_pr"] == 80
    assert success_records[0]["replacement_pr"] == 91


# ── 회귀 6. replacement 실패 → Critical 7종 #N ────────────────────────────────
def test_classify_failure_maps_to_critical_seven(monkeypatch: pytest.MonkeyPatch) -> None:
    runner, _, _, _ = _make_runner()
    # 6-A. push 단계 실패 → REPLACEMENT_PR_ALSO_FAILED (Critical 7종)
    code, is_critical = runner.classify_failure(
        ReplacementFailure(stage="push", reason="git_push_failed"),
    )
    assert code == CRITICAL_REPLACEMENT_FAILED
    assert is_critical is True
    assert code in CRITICAL_CODES

    # 6-B. pr_create 실패 → 동일 매핑
    code, _ = runner.classify_failure(
        ReplacementFailure(stage="pr_create", reason="gh_pr_create_failed"),
    )
    assert code == CRITICAL_REPLACEMENT_FAILED
    assert code in CRITICAL_CODES

    # 6-C. 사전 조건 (token 부재) → EFFECTIVE_DIFF_CONTAMINATION_REPLACEMENT_FAILED
    code, is_critical = runner.classify_failure(
        ReplacementFailure(stage="bot_token", reason="bot_token_unavailable"),
    )
    assert code == CRITICAL_DIFF_REPLACEMENT_FAILED
    assert is_critical is True
    assert code in CRITICAL_CODES

    # 6-D. 실제 실행에서 git push 실패 → ReplacementFailure 반환 + classify Critical
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")
    git_scenarios = [
        _cp(),                                # checkout -b
        _cp(),                                # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                # checkout PR head -- (일괄 stage)
        _cp(),                                # commit
        _cp(returncode=1, stderr="remote rejected"),  # push FAILED
    ]
    git = _GitRecorder(git_scenarios)
    runner2, _, _, _ = _make_runner(git_runner=git)
    failure = runner2.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(failure, ReplacementFailure)
    assert failure.stage == "push"
    code2, _ = runner2.classify_failure(failure)
    assert code2 == CRITICAL_REPLACEMENT_FAILED


# ── 회귀 7. executor 인터페이스 contract dict 키 ────────────────────────────
def test_executor_contract_dict_keys() -> None:
    contamination_dirty = ContaminationReport(
        contaminated=True,
        extra_files=("scripts/leak.py",),
        missing_files=(),
    )
    contract_dirty = ReplacementPRRunner.build_executor_contract(
        contamination=contamination_dirty,
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    # 회장 §명시 contract 키 2개
    assert set(contract_dirty.keys()) == {
        "replacement_pr_required",
        "replacement_pr_runner_input",
    }
    assert contract_dirty["replacement_pr_required"] is True
    payload = contract_dirty["replacement_pr_runner_input"]
    assert payload["original_pr"] == 80
    assert payload["expected_files"] == list(EXPECTED_FILES)
    assert payload["clean_branch_name"] == "task/task-2537-dev4-clean"
    # contamination 세부 정보는 nested dict 로 전달
    assert payload["contamination"]["contaminated"] is True
    assert payload["contamination"]["extra_files"] == ["scripts/leak.py"]

    # contamination 0 인 clean 경로에서는 replacement_pr_required=False
    contamination_clean = ContaminationReport(
        contaminated=False,
        extra_files=(),
        missing_files=(),
    )
    contract_clean = ReplacementPRRunner.build_executor_contract(
        contamination=contamination_clean,
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert contract_clean["replacement_pr_required"] is False


# ── 회귀 8. expected_files 일괄 checkout (Gemini 1차 medium 자동 수용) ────────
def test_create_clean_replacement_uses_batched_checkout(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """expected_files N 개를 단일 git checkout 호출로 stage 한다.

    Gemini 1차 review (review_id=4259255425, 2026-05-10) medium 권고:
      - 기존: expected_files 루프 → git checkout 개별 호출 N회
      - 개선: 단일 호출 1회 (subprocess overhead 절감)
    본 회귀는 batched 동작을 박제하여 회귀 방지.
    """
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")

    expected_three = (
        "anu_v2/replacement_pr_runner.py",
        "anu_v2/tests/test_replacement_pr_runner_2537.py",
        "anu_v2/__init__.py",
    )
    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(),                                                  # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                                  # checkout PR head -- (일괄: 3 files)
        _cp(),                                                  # commit
        _cp(),                                                  # push
        _cp(stdout="cafef00d\n"),                               # rev-parse HEAD
    ]
    gh_scenarios = [
        _cp(stdout="https://github.com/owner/repo/pull/92\n"),
    ]
    git = _GitRecorder(git_scenarios)
    gh = _GhRecorder(gh_scenarios)
    runner, _, _, _ = _make_runner(git_runner=git, gh_runner=gh, audit_root=tmp_path)

    result = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(expected_three),
        clean_branch_name="task/task-2537-dev4-clean-3",
    )
    assert isinstance(result, ReplacementResult)

    # checkout PR head 호출은 정확히 1회 — 개별 호출 N회 회귀 차단.
    pr_head_ref = "refs/pull/80/head"
    pr_head_calls = [
        args for args, _ in git.calls
        if len(args) >= 2 and args[0] == "checkout" and args[1] == pr_head_ref
    ]
    assert len(pr_head_calls) == 1, (
        f"expected 1 batched checkout, got {len(pr_head_calls)} (개별 호출 회귀)"
    )

    # 일괄 호출의 args 끝에 모든 expected_files 가 정확히 포함.
    batched_args = pr_head_calls[0]
    # 형태: ["checkout", "refs/pull/80/head", "--", *expected_three]
    assert batched_args[2] == "--"
    assert tuple(batched_args[3:]) == expected_three


# ── 회귀 9. 일괄 checkout 실패 시 paths 전체가 extra 에 박제 ──────────────────
def test_create_clean_replacement_batched_checkout_failure_reports_all_paths(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """일괄 checkout 실패 시 ReplacementFailure.extra 에 paths(list) 박제."""
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")
    git_scenarios = [
        _cp(),                                                          # checkout -b
        _cp(),                                                          # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(returncode=1, stderr="pathspec did not match any file"),    # batched checkout FAIL
    ]
    git = _GitRecorder(git_scenarios)
    runner, _, _, _ = _make_runner(git_runner=git)

    failure = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(failure, ReplacementFailure)
    assert failure.stage == "checkout"
    assert failure.reason == "git_checkout_path_failed"
    # paths 키에 expected_files 전체가 list 로 박제 (개별 path 누락 방지)
    assert failure.extra.get("paths") == list(EXPECTED_FILES)


# ── 회귀 10. Gemini 2차 high #1 — BOT identity env 주입 박제 ─────────────────
def test_create_clean_replacement_injects_bot_git_identity(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """env 에 GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL/GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL
    4개 키가 모두 주입되었는지 확인. default identity 검증.
    """
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")

    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(),                                                  # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                                  # checkout PR head -- (일괄 stage)
        _cp(),                                                  # commit
        _cp(),                                                  # push
        _cp(stdout="deadbeef\n"),                               # rev-parse HEAD
    ]
    gh_scenarios = [
        _cp(stdout="https://github.com/owner/repo/pull/99\n"),
    ]
    git = _GitRecorder(git_scenarios)
    gh = _GhRecorder(gh_scenarios)
    runner, _, _, _ = _make_runner(git_runner=git, gh_runner=gh, audit_root=tmp_path)

    result = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(result, ReplacementResult)

    # 모든 git/gh 호출의 env 에 BOT identity 4개 키가 주입되었는지 확인 (default 값)
    for _, env in git.calls + gh.calls:
        assert env.get("GIT_AUTHOR_NAME") == "jeon-jonghyuk-taskctl-bot"
        assert env.get("GIT_AUTHOR_EMAIL") == "jeon-jonghyuk-taskctl-bot@users.noreply.github.com"
        assert env.get("GIT_COMMITTER_NAME") == "jeon-jonghyuk-taskctl-bot"
        assert env.get("GIT_COMMITTER_EMAIL") == "jeon-jonghyuk-taskctl-bot@users.noreply.github.com"

    # raw 토큰이 env 에서 GH_TOKEN/GITHUB_TOKEN 이외의 형태로 노출되지 않아야 함
    # (audit_writer 는 env dict 를 직접 기록하지 않으므로 별도 검증 불필요)
    for _, env in git.calls + gh.calls:
        assert "GH_TOKEN" in env
        assert "GITHUB_TOKEN" in env


# ── 회귀 11. Gemini 2차 high #1 — custom BOT identity 반영 박제 ──────────────
def test_create_clean_replacement_custom_bot_identity(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """생성자 인자로 커스텀 bot_git_name/bot_git_email 주입 시 env 에 그 값이 반영되는지 확인."""
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")

    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(),                                                  # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                                  # checkout PR head -- (일괄 stage)
        _cp(),                                                  # commit
        _cp(),                                                  # push
        _cp(stdout="cafebabe\n"),                               # rev-parse HEAD
    ]
    gh_scenarios = [
        _cp(stdout="https://github.com/owner/repo/pull/100\n"),
    ]
    git = _GitRecorder(git_scenarios)
    gh = _GhRecorder(gh_scenarios)

    custom_name = "custom-bot-name"
    custom_email = "custom-bot@example.com"
    root = tmp_path
    runner = ReplacementPRRunner(
        gh_runner=gh,
        git_runner=git,
        audit_writer=_AuditRecorder(),
        audit_root=root,
        bot_token_env="BOT_GITHUB_TOKEN",
        bot_git_name=custom_name,
        bot_git_email=custom_email,
    )

    result = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(result, ReplacementResult)

    for _, env in git.calls + gh.calls:
        assert env.get("GIT_AUTHOR_NAME") == custom_name
        assert env.get("GIT_AUTHOR_EMAIL") == custom_email
        assert env.get("GIT_COMMITTER_NAME") == custom_name
        assert env.get("GIT_COMMITTER_EMAIL") == custom_email


# ── 회귀 12. Gemini 2차 high #2 — fetch 선행 후 checkout 순서 박제 ───────────
def test_create_clean_replacement_fetches_pull_head_before_checkout(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """git_calls 시퀀스에서 fetch 호출이 checkout(PR head) 호출보다 먼저 실행되는지 확인."""
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")

    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(),                                                  # fetch origin refs/pull/80/head:refs/pull/80/head
        _cp(),                                                  # checkout PR head -- (일괄 stage)
        _cp(),                                                  # commit
        _cp(),                                                  # push
        _cp(stdout="aabbccdd\n"),                               # rev-parse HEAD
    ]
    gh_scenarios = [
        _cp(stdout="https://github.com/owner/repo/pull/101\n"),
    ]
    git = _GitRecorder(git_scenarios)
    gh = _GhRecorder(gh_scenarios)
    runner, _, _, _ = _make_runner(git_runner=git, gh_runner=gh, audit_root=tmp_path)

    result = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(result, ReplacementResult)

    # fetch 호출 인덱스 확인
    fetch_ref = "refs/pull/80/head:refs/pull/80/head"
    fetch_indices = [
        i for i, (args, _) in enumerate(git.calls)
        if len(args) >= 3 and args[0] == "fetch" and args[1] == "origin" and args[2] == fetch_ref
    ]
    assert len(fetch_indices) == 1, f"fetch 호출이 정확히 1회여야 함, got {fetch_indices}"

    # checkout PR head 호출 인덱스 확인
    pr_head_ref = "refs/pull/80/head"
    checkout_indices = [
        i for i, (args, _) in enumerate(git.calls)
        if len(args) >= 2 and args[0] == "checkout" and args[1] == pr_head_ref
    ]
    assert len(checkout_indices) == 1, f"checkout PR head 호출이 정확히 1회여야 함, got {checkout_indices}"

    # fetch 가 checkout 보다 먼저 실행되었는지 인덱스로 검증
    assert fetch_indices[0] < checkout_indices[0], (
        f"fetch(idx={fetch_indices[0]}) 이 checkout(idx={checkout_indices[0]}) 보다 먼저 실행되어야 함"
    )


# ── 회귀 13. Gemini 2차 high #2 — fetch 실패 시 stage="fetch" 보고 박제 ──────
def test_create_clean_replacement_fetch_failure_reports_stage_fetch(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """fetch 단계 returncode != 0 시 ReplacementFailure(stage="fetch",
    reason="git_fetch_pull_head_failed", extra.ref 명시) 반환 박제.
    """
    monkeypatch.setenv("BOT_GITHUB_TOKEN", "bot-test-token")
    git_scenarios = [
        _cp(),                                                  # checkout -b
        _cp(returncode=1, stderr="fatal: couldn't find remote ref"),  # fetch FAIL
    ]
    git = _GitRecorder(git_scenarios)
    runner, _, _, _ = _make_runner(git_runner=git)

    failure = runner.create_clean_replacement(
        original_pr=80,
        expected_files=list(EXPECTED_FILES),
        clean_branch_name="task/task-2537-dev4-clean",
    )
    assert isinstance(failure, ReplacementFailure)
    assert failure.stage == "fetch"
    assert failure.reason == "git_fetch_pull_head_failed"
    assert failure.extra.get("ref") == "refs/pull/80/head"
