From 520373a694320da80af99cb201ee5dbc1b44f34f Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 22:17:27 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=511 --- docs/work-items/ORCH-093/01-brd.md | 145 ++++++++++++++++++ docs/work-items/ORCH-093/02-trz.md | 142 +++++++++++++++++ .../ORCH-093/03-acceptance-criteria.md | 114 ++++++++++++++ docs/work-items/ORCH-093/04-test-plan.yaml | 116 ++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 docs/work-items/ORCH-093/01-brd.md create mode 100644 docs/work-items/ORCH-093/02-trz.md create mode 100644 docs/work-items/ORCH-093/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-093/04-test-plan.yaml diff --git a/docs/work-items/ORCH-093/01-brd.md b/docs/work-items/ORCH-093/01-brd.md new file mode 100644 index 0000000..c1a62b5 --- /dev/null +++ b/docs/work-items/ORCH-093/01-brd.md @@ -0,0 +1,145 @@ +--- +work_item: ORCH-093 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 01 — BRD (бизнес-требования): ORCH-093 — merge-актор не ретраит транзиентные ошибки Gitea (405/5xx) → ложный HOLD + мусорные PR + +Work Item: **ORCH-093** · Repo: **orchestrator** · Стадия: analysis + +## 1. Бизнес-контекст и проблема + +Тип: **BUG** (надёжность self-deploy merge-фазы). Найдено по инциденту **ORCH-063 (09.06)**. + +**Инцидент-первоисточник (ORCH-063, 09.06).** Прод-деплой self-hosting прошёл, staging OK, но при +мерже PR в `main` Gitea вернул `HTTP 405 {"message":"Please try again later"}` — транзиентная икота +(Gitea пересчитывал `mergeable` сразу после пуша). PR #98 был `open` + `mergeable=True`, конфликтов +**не было**. Однако merge-актор `merge_gate.merge_pr()` — **one-shot**: на любой не-200/201 он сразу +вернул `(False, "merge failed: HTTP 405")`. Сработала корректная защита ORCH-071/081 «deploy +succeeded but not merged» → задача удержана на `deploy` (НЕ `done`), алерт, потребовался **ручной +домерж** (повтор `merge_pr` вручную → смержилось с первого раза). Защита отработала верно, но +**транзиент не должен был требовать человека**. + +**Два дефекта, оба верифицированы по коду прода `src/merge_gate.py`:** + +- **ДЕФЕКТ 1 — `merge_pr` не ретраит транзиентные HTTP-ошибки.** `merge_gate.merge_pr()` + (`src/merge_gate.py` ~700) делает **один** `POST /pulls/{index}/merge`; на любой не-200/201 + (включая `405 "try again later"`, `5xx`, `409/422` «ещё считается mergeable») сразу + `return False, "merge failed: HTTP {code}"` — без ретрая. Сравни: у Claude-агентов есть + transient-breaker (`429/overload` ретраится), у merge-актора такого механизма нет → инфра-икота + Gitea = ложный HOLD. +- **ДЕФЕКТ 2 — `ensure_open_pr` плодит мусорные PR на уже влитой ветке.** При повторном прогоне + финализатора **после** ручного мержа: PR #98 уже `merged+closed` → `ensure_open_pr` + (`src/merge_gate.py` ~605) не находит открытого code-PR → **создаёт новый пустой PR #99** (ветка + уже в `main`, diff пустой). Пришлось закрывать вручную. + +**Боль:** ложные HOLD при инфра-икоте Gitea требуют ручного вмешательства в автономный конвейер +(эпик ORCH-088 — пакетный автономный прогон) и оставляют мусорные пустые PR. + +## 2. Объём (scope) + +### В объёме +- `merge_pr` ретраит **транзиентные** ошибки мержа (405/«try again», 408, `5xx`, таймаут/сетевые, а + также `409/422` когда PR **всё ещё mergeable**) с ограниченным числом попыток и backoff — **перед** + тем как вернуть `False`. +- Различение «mergeable, но Gitea временно отказал» (ретраить) vs «реальный конфликт / не-mergeable» + (НЕ ретраить, честный быстрый HOLD). +- `ensure_open_pr` / merge-verify **не создаёт** новый PR, если ветка уже полностью в `main` (нет + коммитов `origin/main..branch`) — возвращает исход «already-in-main»; финализатор сразу доводит до + `done` без мусорного PR. +- Конфигурируемость (число ретраев, backoff, kill-switch на ретрай-поведение); разумные дефолты. +- Обновление `.env.example`, `CHANGELOG.md`, merge-gate-раздела документации. + +### Вне объёма +- ❌ Снятие/ослабление защиты ORCH-071/081 «deploy succeeded but not merged» — она корректна; задача + лишь снижает **ложные** срабатывания на транзиентах. +- ❌ Ретрай **реального** конфликта / не-mergeable — это законный HOLD, нужен человек. +- ❌ Любые прямые `push`/`force-push` в `main` (инвариант INV-4 ORCH-071/073 — мерж только через + Gitea PR-merge API). +- ❌ Изменение `STAGE_TRANSITIONS`, состава `QG_CHECKS`, схемы БД. +- ❌ Изменение SHA-in-main-доказательства мержа (`verify_merged_to_main`) как источника истины. + +## 3. Заинтересованные стороны + +- **Заказчик / оператор автономного конвейера (Owner, Стрим)** — меньше ручных домержей, чище список + PR в Gitea. +- **Self-hosting репо `orchestrator`** — основной потребитель merge-verify under-gate (ORCH-071); + изменение в первую очередь касается self-deploy merge-фазы. +- **Все проекты на общем инстансе** — косвенно: меньше зависших на `deploy` задач, держащих + merge-lease и клинящих serial-gate репо (ORCH-088). +- **Reviewer / tester** — принимают результат по AC и зелёному `pytest`. + +## 4. Бизнес-требования (BR) + +- **BR-1** — При транзиентной ошибке мержа (`405`/«Please try again later», `408`, `5xx`, + таймаут/сетевая ошибка) `merge_pr` повторяет `POST …/merge` до `N` раз с backoff, прежде чем + вернуть `(False, …)`; успешный повтор внутри бюджета → `(True, …)`, мерж выполнен. +- **BR-2** — `merge_pr` различает «PR mergeable, Gitea временно отказал» (ретраить) и «реальный + конфликт / PR не mergeable» (НЕ ретраить). Различение опирается на код ответа **и** поле + `mergeable` PR (`GET /pulls/{n}`). Неоднозначный `409/422` классифицируется по `mergeable`. +- **BR-3** — Терминальные ошибки (`404` нет PR / реальный конфликт / `403`) НЕ ретраятся — `merge_pr` + возвращает `(False, …)` быстро; честный HOLD (защита ORCH-071/081) сохраняется. +- **BR-4** — При исчерпании ретраев `merge_pr` возвращает `(False, …)` с понятным reason; защита + «deploy succeeded but not merged» срабатывает как прежде (HOLD + алерт). +- **BR-5** — Если ветка уже полностью в `main` (нет коммитов `origin/main..branch`), `ensure_open_pr` + НЕ создаёт PR — возвращает исход «already-in-main»; merge-verify доводит задачу до `done` без + мусорного пустого PR. +- **BR-6** — Поведение ретрая конфигурируемо: число попыток, backoff и kill-switch; дефолты разумны + (≈3 попытки, backoff 2–5 с) и задокументированы в `.env.example`. +- **BR-7** — При выключенном ретрай-kill-switch поведение `merge_pr` идентично текущему (one-shot) — + нулевая регрессия. + +## 5. Нефункциональные требования (NFR) + +- **NFR-1 (never-raise)** — Контракт never-raise `merge_pr` / `ensure_open_pr` сохранён: любая + HTTP/parse/сетевая ошибка → `(False, …)` / `("failed"|"already-in-main", …)`, исключение никогда не + пробрасывается в `_handle_merge_verify` / `advance_stage`. +- **NFR-2 (self-hosting safety / INV-4)** — Никаких прямых `push`/`force-push` в `main`; мерж только + через Gitea PR-merge API. Прод-контейнер `orchestrator` не перезапускается этой задачей. +- **NFR-3 (обратимость / kill-switch)** — Ретрай-поведение полностью отключаемо одним флагом → + откат к нынешнему one-shot без изменения кода. +- **NFR-4 (ограниченность)** — Суммарное время ретраев ограничено (`N` × backoff_max) и не может + «подвесить» monitor-поток, исполняющий merge-verify; backoff с верхним потолком. +- **NFR-5 (идемпотентность)** — Повторный прогон финализатора на уже влитой ветке безопасен и + бесследен (нет дублей PR, нет дублей мержа — переиспользуется `pr_already_merged`). +- **NFR-6 (наблюдаемость)** — Каждый ретрай и его причина логируются (по образцу `check_ci_green`: + `attempt i/N`); исход (успех/исчерпание/терминал) различим в логе. + +## 6. Допущения и ограничения + +- Gitea-код `405 {"message":"Please try again later"}` — **транзиент** (Gitea пересчитывает + `mergeable` сразу после пуша); `5xx`/таймаут/сетевая — транзиент. +- `409` (conflict) и `422` (unprocessable) **двойственны**: либо реальный конфликт, либо «ещё не + пересчитан mergeable». Источник различения — поле `mergeable` из `GET /pulls/{n}` (а не только + код): `mergeable==True` → транзиент (ретраить), `mergeable==False` → реальный конфликт (НЕ + ретраить). +- `404` (нет PR) обрабатывается раньше шагом «no open PR» и/или трактуется как терминал. +- Образец паттерна ретрая уже есть в репо: `check_ci_green` (`src/qg/checks.py`, attempts + interval + + backoff) и transient-breaker агентов (`backoff_base_seconds`/`backoff_max_seconds`/ + `transient_max_attempts` в `config.py`). +- Merge-verify under-gate (ORCH-071) реален только для self-hosting (`merge_verify_applies`); на + прочих репо мерж делает LLM-deployer — там изменение `merge_pr` не задействуется. +- Изменение **точечное** в `src/merge_gate.py` + флаги в `src/config.py`; `STAGE_TRANSITIONS`, + `QG_CHECKS`, схема БД не трогаются. + +## 7. Критерии успеха + +`merge_pr` переживает транзиентную икоту Gitea (405/5xx/таймаут/«not mergeable yet») за счёт +ограниченного ретрая с backoff и больше не даёт ложного HOLD; реальный конфликт по-прежнему даёт +быстрый честный HOLD; `ensure_open_pr` не создаёт мусорных PR на уже влитой ветке; поведение +конфигурируемо и отключаемо; never-raise сохранён; `pytest tests/ -q` зелёный; доки и `.env.example` +обновлены. Детальные PASS/FAIL — `03-acceptance-criteria.md`. + +## 8. Риски + +- Слишком агрессивный ретрай реального конфликта → задержка честного HOLD (митигируется BR-2/BR-3: + классификация по `mergeable`). +- Ошибочная классификация транзиента как терминала (или наоборот) при неполном ответе Gitea + (`mergeable=None`) — нужна осторожная дефолт-политика. +- Гонка `ensure_open_pr` already-in-main vs параллельный мерж. + +Детали и оценка — `10-tech-risks.md` (заполняет архитектор). diff --git a/docs/work-items/ORCH-093/02-trz.md b/docs/work-items/ORCH-093/02-trz.md new file mode 100644 index 0000000..beec2a8 --- /dev/null +++ b/docs/work-items/ORCH-093/02-trz.md @@ -0,0 +1,142 @@ +--- +work_item: ORCH-093 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 02 — ТЗ (TRZ): ORCH-093 — merge-актор ретраит транзиентные ошибки Gitea + гард «ветка уже в main» + +Work Item: **ORCH-093** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода +> (`src/merge_gate.py`, `src/config.py`, `src/stage_engine.py`). Архитектурное обоснование (точный +> алгоритм классификации, формат хелпера, выбор дефолтов) — задача архитектора (`06-adr`). + +## 1. Сводка изменения + +Две точечные доработки `src/merge_gate.py`: + +1. **`merge_pr` (~700)** — обернуть `POST /pulls/{index}/merge` в **retry-loop** на транзиентных + кодах (`405`/«try again», `408`, `5xx`, таймаут/сетевые, плюс `409/422` при `mergeable==True`) с + ограниченным числом попыток и backoff; **терминальные** исходы (`404` нет PR, реальный конфликт / + `mergeable==False`, `403`) → быстрый `(False, …)` без ретрая. По образцу `check_ci_green` + (attempts + interval) и transient-breaker агентов. +2. **`ensure_open_pr` (~605)** — добавить гард «ветка уже полностью в `main`» (нет коммитов + `origin/main..branch`) → новый исход `"already-in-main"` **до** создания PR; в + `_handle_merge_verify` этот исход трактуется как «мерж уже состоялся» → SHA-in-main подтверждает → + `done` без мусорного PR. + +Новые флаги ретрая в `src/config.py` (`ORCH_MERGE_RETRY_*`) + дескрипторы в `.env.example`. Контракт +never-raise и INV-4 (никогда не `push`/`force-push` `main`) — сохраняются. `STAGE_TRANSITIONS`, +`QG_CHECKS`, схема БД — **не трогаются**. + +## 2. Задействованные модули / пути + +| Путь | Действие | +|------|----------| +| `src/merge_gate.py` | изменить — `merge_pr` (retry-loop + классификатор транзиент/терминал); `ensure_open_pr` (гард already-in-main); при необходимости leaf-хелперы `_is_transient_merge_error()` / `_branch_fully_in_main()` | +| `src/config.py` | изменить — добавить флаги ретрая мержа (`merge_retry_enabled`, `merge_retry_max_attempts`, `merge_retry_backoff_base_s`, `merge_retry_backoff_max_s`) по образцу `ci_poll_*` / `merge_pr_timeout_s` | +| `src/stage_engine.py` | изменить (точечно) — `_handle_merge_verify` (~1447): обработать новый исход `ensure_open_pr == "already-in-main"` как «мерж уже состоялся» (пропустить `merge_pr`, дать `verify_merged_to_main` подтвердить → `done`) | +| `.env.example` | изменить — новые дескрипторы `ORCH_MERGE_RETRY_*` | +| `tests/test_merge_gate.py` | изменить — мок httpx-последовательностей (405×2→200; конфликт; already-in-main; исчерпание; kill-switch off) | +| `CHANGELOG.md` | изменить — запись ORCH-093 | +| `docs/architecture/README.md` (merge-gate раздел) / `CLAUDE.md` | изменить — описать ретрай и гард already-in-main | + +## 3. Функциональные требования + +### FR-1 — retry-loop транзиентных ошибок мержа в `merge_pr` (BR-1, BR-4, BR-6, BR-7) +- Шаги `merge_pr` до `POST` (idempotency-guard `pr_already_merged`; `GET …/pulls?state=open` поиск + code-PR `head==branch AND base==main`; `index is None → (False, "no open PR")`) — **без изменений**. +- `POST /pulls/{index}/merge` выполняется в цикле до `merge_retry_max_attempts` попыток (дефолт `3`): + - `200/201` → `(True, "merged PR #")` (немедленный выход). + - **транзиентный** исход (см. FR-2) И остались попытки → `sleep(backoff)` и повтор `POST`; + `backoff` экспоненциальный от `merge_retry_backoff_base_s` (дефолт `2`) с потолком + `merge_retry_backoff_max_s` (дефолт `5`). + - **терминальный** исход (см. FR-2) → немедленно `(False, "merge failed: HTTP ")` без + дальнейших попыток. + - исчерпание попыток на транзиенте → `(False, "merge failed after attempts: HTTP ")`. +- **Kill-switch** `merge_retry_enabled=False` → ровно одна попытка `POST` (текущее one-shot + поведение, BR-7). +- Каждая попытка логируется (`attempt i/N`, код, transient/terminal) — образец `check_ci_green`. + +### FR-2 — классификация транзиент vs терминал (BR-2, BR-3) +- **Транзиентные** (ретраить): `405` («Please try again later»), `408` (timeout), любой `5xx`, + `httpx`-таймаут / сетевая ошибка, **и** `409`/`422` когда PR **всё ещё mergeable**. +- **Терминальные** (НЕ ретраить, быстрый `False`): `403` (нет прав), `404` (PR исчез), и `409`/`422` + при **реальном конфликте** (`mergeable==False`). +- Различение неоднозначного `409`/`422`: дополнительный `GET /pulls/{index}` → поле `mergeable`: + - `mergeable==True` → транзиент (Gitea ещё не пересчитал) → ретрай. + - `mergeable==False` → реальный конфликт → терминал. + - `mergeable` отсутствует/`None` → консервативная дефолт-политика (рекомендация аналитика: + трактовать как транзиент с тем же ограниченным бюджетом ретраев, т.к. сетевая икота Gitea — + наблюдаемый кейс; финальное решение — архитектор в `06-adr`). +- Сетевые/таймаут-исключения `httpx` внутри попытки ловятся (never-raise) и классифицируются как + транзиент в рамках того же бюджета. + +### FR-3 — гард «ветка уже полностью в main» в `ensure_open_pr` (BR-5) +- Перед шагом «создать PR» (после того как открытый code-PR не найден) `ensure_open_pr` проверяет, + что в ветке нет коммитов сверх `origin/main`: в per-branch worktree `git fetch origin main` + + `git rev-list --count origin/main..` (или `git merge-base --is-ancestor origin/main`). + - count `== 0` (ветка целиком в `main`) → `("already-in-main", "")` — **PR не создаётся**. + - count `> 0` (есть невлитые коммиты) → текущий путь `POST …/pulls` (создать code-PR). + - git/OS ошибка проверки → **не** блокировать (never-raise); деградировать на текущее поведение + (попытаться создать PR) ИЛИ вернуть `failed` — точную fail-политику фиксирует архитектор. Гард + не должен превратить инфра-икоту git в ложный no-op мержа. +- Сигнатура возврата `ensure_open_pr` расширяется новым статусом `"already-in-main"` дополнительно к + `"existed"|"created"|"failed"` (обратносовместимо для существующих веток вызова). + +### FR-4 — обработка `already-in-main` в `_handle_merge_verify` (BR-5) +- В `stage_engine._handle_merge_verify` (~1487): при `pr_status == "already-in-main"` — + логировать, **пропустить** `merge_gate.merge_pr` (мержить нечего) и перейти сразу к + `verify_merged_to_main` (SHA-in-main подтвердит факт мержа → `done`). Это НЕ `failed`-ветка (не + HOLD): ветка уже в `main`, цель достигнута. +- SHA-in-main (`verify_merged_to_main`) остаётся **авторитетным** доказательством мержа; гард только + избегает мусорного PR и лишнего `merge_pr`. + +### FR-5 — конфигурация и обратная совместимость (BR-6, BR-7) +- Новые поля `settings` (см. §2) с дефолтами; читаются из env (`ORCH_MERGE_RETRY_*`). +- При `merge_retry_enabled=False` — поведение `merge_pr` байт-в-байт как сейчас (one-shot). +- Гард already-in-main также под флагом ИЛИ всегда-вкл (рекомендация: всегда-вкл, т.к. он лишь + предотвращает создание заведомо пустого PR; решение — архитектор). + +## 4. Изменения API + +Нет (внешних HTTP-эндпоинтов оркестратора не добавляется/не меняется). Меняется только клиентское +обращение к Gitea API внутри `merge_gate` (дополнительный `GET /pulls/{index}` для чтения +`mergeable` при неоднозначном `409/422`; ретрай `POST …/merge`). Read-only блок merge-verify в +`GET /queue` (`merge_verify_status()`) опционально может получить счётчик ретраев (необязательно). + +## 5. Изменения схемы БД + +Нет. (Merge-lease — файловый, не БД; счётчики `_MERGE_VERIFY_COUNTERS` — in-process. Новые поля — +только в `config.Settings`, не в схеме.) + +## 6. Требования к новым/изменённым QG checks + +Нет. `STAGE_TRANSITIONS`, состав `QG_CHECKS`, exit-гейты рёбер и под-гейты ребра +`deploy-staging → deploy` — **не трогаются**. Изменение целиком внутри детерминированного +merge-актора `merge_pr`/`ensure_open_pr` (под-гейт-врезка `_handle_merge_verify` ребра +`deploy → done`), который НЕ зарегистрирован в `QG_CHECKS`. + +## 7. Совместимость / регресс + +- **Kill-switch `merge_retry_enabled=False`** → one-shot `merge_pr` (текущее поведение) — нулевая + регрессия. +- **Защита ORCH-071/081** «deploy succeeded but not merged» сохраняется 1:1: после исчерпания + ретраев / на терминальном конфликте `merge_pr` возвращает `False`, и при неподтверждённом + SHA-in-main срабатывает прежний HOLD + алерт. +- **INV-4 / self-hosting safety**: никаких `push`/`force-push` в `main`; мерж только через Gitea + PR-merge API; прод-контейнер не перезапускается. +- **never-raise**: `merge_pr` / `ensure_open_pr` ловят все исключения и возвращают безопасный + кортеж — контракт сохранён (тесты на never-raise остаются зелёными). +- **Идемпотентность**: `pr_already_merged` (idempotency-guard) и гард already-in-main делают + повторный прогон финализатора бесследным (нет дублей PR/мержей). +- **Область раската**: реально задействуется на merge-verify under-gate (self-hosting, + `merge_verify_applies`); на прочих репо merge делает LLM-deployer — изменение нейтрально. +- **Артефакты pipeline**: создаётся/обновляется только аналитический пакет (`01`–`04`); в + development-стадии обновятся `CHANGELOG.md`, `.env.example`, merge-gate-раздел доки. ADR + (`06-adr/`) — пишет архитектор. +- Полный регресс `pytest tests/ -q` должен оставаться зелёным. diff --git a/docs/work-items/ORCH-093/03-acceptance-criteria.md b/docs/work-items/ORCH-093/03-acceptance-criteria.md new file mode 100644 index 0000000..24f9136 --- /dev/null +++ b/docs/work-items/ORCH-093/03-acceptance-criteria.md @@ -0,0 +1,114 @@ +--- +work_item: ORCH-093 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-09 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-093 — ретрай транзиентных merge-ошибок Gitea + гард already-in-main + +Work Item: **ORCH-093** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что +считается провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам. + +--- + +## AC-1 — ретрай транзиента 405/5xx/таймаут → успешный мерж + +**Условие:** `merge_pr` при транзиентной ошибке мержа повторяет `POST …/merge` с backoff и +доводит мерж до успеха в пределах бюджета. +- **PASS:** мок httpx даёт на `POST …/merge` `405` дважды, затем `200` → `merge_pr` возвращает + `(True, …)`, выполнено ровно 3 `POST`, ложного `False` нет. Аналогично для `5xx` и + таймаута/сетевой ошибки в первых попытках. +- **FAIL:** `merge_pr` возвращает `False` на первом `405`/`5xx`/таймауте (one-shot), не делая + повторных `POST`. + +--- + +## AC-2 — реальный конфликт / не-mergeable НЕ ретраится (быстрый честный HOLD) + +**Условие:** `merge_pr` при реальном конфликте (`409`/`422` с `mergeable==False`) или `403` не +зацикливается, а возвращает `(False, …)` быстро. +- **PASS:** мок httpx даёт `409` на `POST …/merge` и `GET /pulls/{n}` с `mergeable=False` → + `merge_pr` возвращает `(False, …)` без дополнительных `POST` (не более одной попытки мержа); + reason различим как терминальный. `403` → немедленный `(False, …)`. +- **FAIL:** `merge_pr` ретраит реальный конфликт до исчерпания бюджета (вечный/долгий цикл), + задерживая честный HOLD. + +--- + +## AC-3 — исчерпание ретраев → (False, …) + защита ORCH-071/081 как прежде + +**Условие:** если транзиент не проходит за `N` попыток, `merge_pr` возвращает `(False, …)` с +понятным reason; защита «deploy succeeded but not merged» срабатывает как раньше. +- **PASS:** мок даёт `405` на всех `N` попытках → `merge_pr` возвращает + `(False, "merge failed after attempts: HTTP 405")` (или эквивалент); в `_handle_merge_verify` + неподтверждённый SHA-in-main → HOLD + алерт (поведение ORCH-071/081 неизменно). Тест на + не-merged HOLD остаётся зелёным. +- **FAIL:** при исчерпании ретраев reason неинформативен; или защита HOLD не срабатывает / задача + ошибочно уходит в `done`. + +--- + +## AC-4 — гард «ветка уже в main» → нет мусорного PR, задача доходит до done + +**Условие:** если ветка уже полностью в `main` (нет коммитов `origin/main..branch`), +`ensure_open_pr` не создаёт PR и возвращает `already-in-main`; финализатор доводит до `done`. +- **PASS:** мок: открытого code-PR нет, `git rev-list --count origin/main..branch == 0` → + `ensure_open_pr` возвращает `("already-in-main", …)` и **не делает** `POST …/pulls`; в + `_handle_merge_verify` этот статус пропускает `merge_pr` и `verify_merged_to_main` (SHA-in-main) + подтверждает мерж → задача доходит до `done` без создания пустого PR. +- **FAIL:** `ensure_open_pr` создаёт новый пустой PR на уже влитой ветке, либо статус + `already-in-main` ошибочно трактуется как `failed` (ложный HOLD). + +--- + +## AC-5 — kill-switch / конфиг ретраев; дефолты задокументированы + +**Условие:** ретрай-поведение конфигурируемо (число попыток, backoff, kill-switch); при выключении — +one-shot как сейчас; дефолты в `.env.example`. +- **PASS:** в `src/config.py` есть поля `merge_retry_enabled` / `merge_retry_max_attempts` / + `merge_retry_backoff_base_s` / `merge_retry_backoff_max_s` с разумными дефолтами (≈3 / 2 / 5); + `.env.example` содержит дескрипторы `ORCH_MERGE_RETRY_*`; при `merge_retry_enabled=False` тест + подтверждает ровно одну попытку `POST` (one-shot). +- **FAIL:** ретрай захардкожен (нет флагов/kill-switch), или `.env.example` не обновлён, или при + выключенном флаге поведение отличается от текущего one-shot. + +--- + +## AC-6 — never-raise сохранён; регресс зелёный; доки обновлены + +**Условие:** контракт never-raise `merge_pr`/`ensure_open_pr` цел; полный регресс зелёный; +документация и `CHANGELOG` обновлены. +- **PASS:** при любой HTTP/parse/сетевой ошибке (в т.ч. внутри ретрай-цикла и git-проверки гарда) + функции возвращают безопасный кортеж, исключение не пробрасывается; `pytest tests/ -q` зелёный; + merge-gate-раздел доки (`docs/architecture/README.md` / `CLAUDE.md`) и `CHANGELOG.md` описывают + ретрай и гард already-in-main. +- **FAIL:** исключение пробрасывается в `advance_stage`; падает любой тест в `tests/`; доки/CHANGELOG + не отражают изменение. + +--- + +## AC-7 — инварианты self-hosting / INV-4 не нарушены + +**Условие:** изменение не вводит прямых `push`/`force-push` в `main` и не трогает +`STAGE_TRANSITIONS`/`QG_CHECKS`/схему БД. +- **PASS:** мерж по-прежнему идёт только через Gitea PR-merge API; `git diff` не содержит правок + `STAGE_TRANSITIONS` / состава `QG_CHECKS` / схемы БД; никаких новых вызовов `git push … main`. +- **FAIL:** появился прямой push в `main`, либо изменены `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1, FR-2 | +| AC-2 | BR-2, BR-3 / FR-2 | +| AC-3 | BR-4 / FR-1 | +| AC-4 | BR-5 / FR-3, FR-4 | +| AC-5 | BR-6, BR-7 / FR-5 | +| AC-6 | NFR-1, NFR-6 / FR-1…FR-5 | +| AC-7 | NFR-2 / §6, §7 ТЗ | diff --git a/docs/work-items/ORCH-093/04-test-plan.yaml b/docs/work-items/ORCH-093/04-test-plan.yaml new file mode 100644 index 0000000..8d68244 --- /dev/null +++ b/docs/work-items/ORCH-093/04-test-plan.yaml @@ -0,0 +1,116 @@ +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