work_item: ORCH-093
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
title: "Ретрай транзиентных merge-ошибок Gitea (405/5xx) + гард already-in-main"
framework: pytest
scope: >
  Покрывает src/merge_gate.py::merge_pr (retry-loop + классификация транзиент/терминал) и
  ensure_open_pr (гард «ветка уже в main»), новые флаги src/config.py (ORCH_MERGE_RETRY_*) и
  обработку already-in-main в stage_engine._handle_merge_verify. Вне покрытия: реальная сеть Gitea,
  STAGE_TRANSITIONS/QG_CHECKS, схема БД.
notes: >
  httpx мокается monkeypatch'ем (по образцу tests/test_merge_gate.py / test_orch073_merge_pr.py):
  последовательности ответов на POST /pulls/{n}/merge и GET /pulls/{n}. time.sleep патчится в no-op,
  чтобы backoff не замедлял тесты. git-операции гарда (rev-list/merge-base) мокаются через
  monkeypatch subprocess.run. Полный регресс tests/ должен оставаться зелёным; считается регрессом
  любое падение существующих test_merge_gate*/test_merge_verify*/test_orch073*.

tests:
  - id: TC-01
    type: unit
    description: "merge_pr: POST даёт 405,405,200 -> возвращает (True, merged PR #n); ровно 3 POST; ложного False нет (AC-1)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-02
    type: unit
    description: "merge_pr: POST даёт 503 (5xx), затем 200 -> ретрай -> (True, ...) (AC-1)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-03
    type: unit
    description: "merge_pr: POST бросает httpx Timeout/сетевую ошибку в 1-й попытке, затем 200 -> ретрай -> (True, ...); never-raise (AC-1, AC-6)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-04
    type: unit
    description: "merge_pr: реальный конфликт 409 + GET /pulls/{n} mergeable=False -> (False, ...) без доп. POST (терминал, быстрый HOLD) (AC-2)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-05
    type: unit
    description: "merge_pr: неоднозначный 409 + GET /pulls/{n} mergeable=True -> классифицирован как транзиент -> ретрай -> 200 -> (True, ...) (AC-2)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-06
    type: unit
    description: "merge_pr: 403 (нет прав) -> немедленно (False, ...) без ретрая (терминал) (AC-2)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-07
    type: unit
    description: "merge_pr: 405 на всех N попытках -> (False, 'merge failed after N attempts: HTTP 405') с понятным reason (AC-3)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-08
    type: unit
    description: "merge_pr: kill-switch merge_retry_enabled=False -> ровно один POST (one-shot, как сейчас) при 405 -> (False, ...) (AC-5, AC-3)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-09
    type: unit
    description: "ensure_open_pr: открытого code-PR нет, rev-list --count origin/main..branch == 0 -> ('already-in-main', ...); POST /pulls НЕ вызывается (AC-4)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-10
    type: unit
    description: "ensure_open_pr: открытого PR нет, есть невлитые коммиты (count>0) -> создаёт PR ('created', ...) (регресс прежнего поведения) (AC-4)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-11
    type: unit
    description: "ensure_open_pr: git-ошибка проверки гарда -> never-raise, безопасный кортеж, без падения (AC-6)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-12
    type: unit
    description: "merge_pr/ensure_open_pr: любая непойманная httpx/parse ошибка -> (False/failed, ...) кортеж, исключение не пробрасывается (never-raise) (AC-6)"
    module: tests/test_merge_gate.py
    expected: PASS

  - id: TC-13
    type: unit
    description: "config: дефолты merge_retry_enabled/merge_retry_max_attempts/backoff_base/backoff_max присутствуют и читаются из ORCH_MERGE_RETRY_* env (AC-5)"
    module: tests/test_config.py
    expected: PASS

  - id: TC-14
    type: integration
    description: "_handle_merge_verify: ensure_open_pr -> 'already-in-main' пропускает merge_pr, verify_merged_to_main (SHA-in-main) подтверждает -> задача доходит до done без мусорного PR (AC-4)"
    module: tests/test_merge_verify.py
    expected: PASS

  - id: TC-15
    type: integration
    description: "_handle_merge_verify: merge_pr исчерпал ретраи (False) и SHA-in-main не подтверждён -> HOLD + alert (ORCH-071/081 как прежде), задача удержана на deploy, не done (AC-3)"
    module: tests/test_merge_verify.py
    expected: PASS

  - id: TC-16
    type: integration
    description: "_handle_merge_verify happy-path: транзиент 405x2->200 в merge_pr -> SHA-in-main подтверждён -> done без ложного HOLD (end-to-end под-гейта deploy->done) (AC-1)"
    module: tests/test_merge_verify.py
    expected: PASS
