diff --git a/docs/work-items/ORCH-043/01-brd.md b/docs/work-items/ORCH-043/01-brd.md new file mode 100644 index 0000000..25c93c4 --- /dev/null +++ b/docs/work-items/ORCH-043/01-brd.md @@ -0,0 +1,114 @@ +# 01 — Business Requirements Document (BRD) + +**Work Item:** ORCH-043 +**Тема:** Безопасная параллель в одном репо: merge-gate + auto-rebase + re-test +**Проект:** orchestrator (self-hosting) +**Автор:** Analyst +**Дата:** 2026-06-06 + +--- + +## 1. Контекст и проблема + +Оркестратор ведёт несколько work item **параллельно**, каждый в своём изолированном +git worktree / ветке (`feature/ORCH-NNN-slug`, ORCH-2/S-4). Все ветки одного проекта +исходят из общего `origin/main` и в конце конвейера **вливаются обратно в `main`**. + +Текущий конвейер валидирует ветку **относительно того состояния `main`, из которого +она была создана**, а не относительно `main` на момент слияния: + +- `check_ci_green` (стадия `development`) — CI зелёный **на ветке** (Gitea commit status ветки). +- `check_tests_passed` (стадия `testing`) — вердикт тестировщика по коду **ветки**. +- На стадии `deploy` ветка вливается в `main` (слияние выполняет deployer-агент, + см. `src/webhooks/gitea.py` — комментарий про «deployer merges the PR at the START of its run»). + +**Между «ветка проверена» и «ветка влита» `main` мог уйти вперёд** из-за слияния другой +параллельной задачи. Возникает **семантический (логический) конфликт слияния**: git +сливает ветки без текстового конфликта, но объединённый код `main` сломан — тесты, +которые были зелёными на ветке, на обновлённом `main` падают. + +### Почему это критично именно здесь (self-hosting) +Проект ORCH правит инструмент, который СЕЙЧАС работает в проде и обслуживает другие +проекты (enduro-trails) из одного инстанса с общей БД и общей очередью (см. `CLAUDE.md`, +`docs/operations/INFRA.md`). Сломанный `main` оркестратора = встал конвейер ВСЕХ проектов. +Две параллельные ORCH-задачи, каждая «зелёная» по отдельности, при последовательном +слиянии способны положить прод. + +### Сценарий-иллюстрация +1. Задачи A и B ответвлены от `main@C0`. +2. A проходит конвейер, вливается → `main@C1`. +3. B тестировалась против `C0`; её CI зелёный относительно `C0`. Git-слияние B в `C1` + проходит без текстового конфликта, но `C1` содержит изменения A, ломающие B. +4. `main` становится красным. Конвейер всех проектов деградирует. + +--- + +## 2. Цель + +Гарантировать, что ветка вливается в `main` **только если она проверена против +актуального `origin/main`**. Перед слиянием ветка автоматически догоняет `main` +(auto-rebase) и **повторно тестируется** (re-test); зелёный результат на актуальном +`main` — обязательное условие слияния (merge-gate). Слияния в `main` одного репозитория +**сериализуются**, чтобы окно гонки не воспроизводилось между двумя гейтами. + +## 3. Заинтересованные стороны +- **Owner / разработчики** — не хотят красный `main` и ручные разборы конфликтов. +- **Все проекты на инстансе** — зависят от живого прод-оркестратора. +- **Агенты конвейера** — получают детерминированный гейт вместо ручной координации. + +## 4. Объём (Scope) + +### В объёме +1. **Merge-gate** — детерминированный гейт перед слиянием в `main`: пропускает + слияние только если ветка не отстаёт от `origin/main` И повторная проверка зелёная. +2. **Auto-rebase** — если ветка отстаёт от `origin/main`, автоматически догнать `main` + (rebase/merge ветки на актуальный `origin/main`) в worktree и запушить результат. +3. **Re-test** — после auto-rebase повторно прогнать тест-набор на догнанной ветке; + зелёный результат — условие прохода гейта. +4. **Сериализация слияний** — в пределах одного репозитория одновременно «догон+слияние» + выполняет только одна задача (merge-lock), иначе гонка воспроизводится. +5. **Откаты при неуспехе** — текстовый конфликт rebase ИЛИ красный re-test → возврат + задачи на `development` (по образцу существующих откатов) с понятным комментарием. +6. **Конфигурируемость** — пороги/тайм-ауты re-test и поведение гейта вынесены в `settings`. + +### Вне объёма +- Изменение логики стадий `analysis` / `architecture` / `review`. +- Замена самого механизма слияния PR в Gitea (UI/настройки репозитория). +- Реальные прод-деплои (остаются за `scripts/orchestrator-deploy-hook.sh`). +- Кросс-репозиторная сериализация (гейт защищает `main` каждого репо отдельно). + +## 5. Бизнес-требования (BR) + +| ID | Требование | +|----|------------| +| BR-1 | Перед слиянием ветки в `main` оркестратор обязан проверить, что ветка содержит последний `origin/main` (не отстаёт). | +| BR-2 | Если ветка отстаёт — оркестратор автоматически догоняет её до `origin/main` без участия человека (auto-rebase). | +| BR-3 | После догона тест-набор повторно прогоняется; слияние разрешено только при зелёном результате (re-test). | +| BR-4 | Текстовый конфликт при auto-rebase или красный re-test НЕ приводит к слиянию: задача откатывается на `development` для ручного фикса. | +| BR-5 | В пределах одного репозитория «догон+проверка+слияние» сериализуются: две задачи не могут одновременно пройти merge-gate и влиться. | +| BR-6 | Гейт детерминированный (Python/гит-команды + код тестов), а не доверие LLM-агенту. | +| BR-7 | Гейт обязателен минимум для self-hosting репозитория `orchestrator`; применим к любому репо с параллельными задачами. | +| BR-8 | Все события гейта (догон, re-test, проход/откат) логируются и отражаются комментарием в Plane, без рассинхрона стадий. | + +## 6. Критерии успеха +- Воспроизводимый ранее сценарий «две зелёные ветки ломают `main`» более не приводит + к красному `main`: вторая ветка либо догоняется и проходит re-test, либо откатывается. +- Прод-контейнер `orchestrator` не перезапускается и не падает в рамках задачи. +- Реестр гейтов и стадий остаётся консистентным (snapshot-тесты обновлены осознанно). + +## 7. Риски и ограничения +- **Гонка между двумя гейтами** — снимается merge-lock (BR-5); без него фикс неполон. +- **Долгий re-test** — нужен тайм-аут и понятный откат, а не вис задачи. +- **Force-push догнанной ветки** — допустим только `--force-with-lease` и только по + own-ветке задачи; никогда по `main`. +- **Self-hosting** — любые изменения не должны ронять/рестартить прод-оркестратор; + обязательная страховка стадией `deploy-staging` (порт 8501) сохраняется. +- Окончательное место встройки в конвейер (новая стадия / гейт существующего перехода / + шаг перед слиянием) — **решение архитектора** (ADR), BRD фиксирует требуемое поведение. + +## 8. Связанные артефакты +- `02-trz.md` — техническое задание (модули, гейт, конфиг, точки встройки). +- `03-acceptance-criteria.md` — критерии приёмки PASS/FAIL. +- `04-test-plan.yaml` — план тестов. +- Контекст кода: `src/qg/checks.py`, `src/stage_engine.py`, `src/git_worktree.py`, + `src/agents/launcher.py`, `src/webhooks/gitea.py`, `src/stages.py`, `src/config.py`. diff --git a/docs/work-items/ORCH-043/02-trz.md b/docs/work-items/ORCH-043/02-trz.md new file mode 100644 index 0000000..4b6cba7 --- /dev/null +++ b/docs/work-items/ORCH-043/02-trz.md @@ -0,0 +1,161 @@ +# 02 — Техническое задание (ТЗ) + +**Work Item:** ORCH-043 +**Тема:** merge-gate + auto-rebase + re-test (безопасная параллель в одном репо) +**Автор:** Analyst + +> ТЗ описывает ТРЕБУЕМОЕ поведение и конкретные точки изменения кода. Окончательный +> выбор места встройки в конвейер (новая стадия vs гейт существующего перехода vs шаг +> перед слиянием) и детали reconciliation — **за архитектором** (ADR в `06-adr/`). +> Если ТЗ окажется нереализуемым — вернуть на стадию `analysis`, не комментировать задним числом. + +--- + +## 1. Задействованные модули `src/` + +| Модуль | Роль в изменении | +|--------|------------------| +| `src/merge_gate.py` (**новый**) | Ядро фичи: ancestor-check, auto-rebase, re-test, merge-lock. Чистые функции + git-операции в worktree. | +| `src/qg/checks.py` | Новый QG-check `check_branch_mergeable` (merge-gate) + регистрация в `QG_CHECKS`. Переиспользует паттерн `check_tests_local` (pytest в worktree) и `_repo_path`. | +| `src/stages.py` | Встройка merge-gate в `STAGE_TRANSITIONS` (точное место — за архитектором; см. §6). | +| `src/stage_engine.py` | Ветка отката merge-gate → `development` в `_handle_qg_failure_rollbacks` + диспетчеризация нового check в `_run_qg`. | +| `src/git_worktree.py` | Возможные хелперы: проверка «behind origin/main», rebase, push `--force-with-lease`. Не ломать сигнатуры `ensure_worktree` / `get_worktree_path`. | +| `src/config.py` | Новые `settings`: тайм-аут re-test, вкл/выкл гейта, политика отстающей ветки, тайм-аут lock. | +| `src/agents/launcher.py` | Если merge-gate встраивается как шаг перед слиянием на стадии `deploy` — точка, где deployer запускается, может потребовать координации с lock (за архитектором). | +| `tests/` | Новые тесты (см. `04-test-plan.yaml`) + обновление snapshot-тестов реестра/стадий. | + +## 2. Функциональные требования к `src/merge_gate.py` + +Предлагаемый публичный контракт (имена финализирует архитектор; поведение обязательно): + +### 2.1 `branch_is_behind_main(repo, branch) -> bool` +- `git fetch origin main` в main-clone/worktree (best-effort, never-raise → трактуем + как «не удалось определить» и НЕ пропускаем слияние вслепую). +- Ветка считается отстающей, если `origin/main` **не** является предком HEAD ветки + (`git merge-base --is-ancestor origin/main ` → ненулевой код). + +### 2.2 `auto_rebase_onto_main(repo, branch) -> (ok: bool, reason: str)` +- Выполняется в изолированном worktree ветки (`ensure_worktree`), НЕ в общем clone. +- Догнать ветку до `origin/main` (rebase либо merge — выбор архитектора; критично: + результат содержит весь `origin/main` и историю/изменения ветки). +- **Текстовый конфликт** → отменить операцию (`git rebase --abort` / `git merge --abort`), + worktree оставить чистым, вернуть `(False, "rebase conflict: <файлы>")`. +- **Чистый догон** → `git push --force-with-lease origin ` (ТОЛЬКО ветка задачи, + НИКОГДА `main`). Вернуть `(True, ...)`. +- Контракт never-raise: любая git/OS-ошибка → `(False, "")`, не исключение. + +### 2.3 `retest_branch(repo, branch) -> (ok: bool, reason: str)` +- Прогнать тест-набор проекта в worktree догнанной ветки. Канон — как в + `check_tests_local`: `python -m pytest` (точная команда/каталог — за архитектором, + согласованно с CI-конфигом `.gitea/workflows/`). +- Тайм-аут `settings.merge_retest_timeout_s`; превышение → `(False, "re-test timeout")`. +- Возврат: `(True, "re-test green")` при коде 0, иначе `(False, "re-test failed: ")`. + +### 2.4 Merge-lock (сериализация, BR-5) +- Реализовать межзадачную сериализацию «догон+re-test+слияние» в пределах одного `repo`. +- Допустимые реализации (выбор архитектора): файловый lock в `repos_dir`, advisory-lock, + либо строка-замок в SQLite. Требования: restart-safe, с тайм-аутом + `settings.merge_lock_timeout_s`, корректное освобождение при ошибке/падении. +- Под локом: повторно сверить «не отстаёт» ПОСЛЕ захвата (double-check), т.к. `main` + мог уйти, пока ждали lock. + +## 3. Новый QG-check (`src/qg/checks.py`) + +``` +check_branch_mergeable(repo, work_item_id, branch) -> tuple[bool, str] +``` + +Поведение (детерминированно, без участия LLM): +1. Захватить merge-lock для `repo` (с тайм-аутом). Не удалось → `(False, "merge-lock busy")`. +2. Если ветка не отстаёт от `origin/main` → `(True, "branch up-to-date with main")`. +3. Иначе `auto_rebase_onto_main`: + - конфликт → `(False, "rebase conflict: ...")`; + - успех → `retest_branch`: + - зелёный → `(True, "rebased onto main, re-test green")`; + - красный/тайм-аут → `(False, "re-test failed after rebase: ...")`. +4. Освободить lock в `finally`. +- Зарегистрировать в `QG_CHECKS` под ключом `"check_branch_mergeable"`. +- Контракт never-raise (как у соседних чеков): исключение → `(False, "")`. + +> **Опционально (за архитектором):** флаг `settings.merge_gate_enabled`; при `False` +> чек возвращает `(True, "merge-gate disabled")` (безопасный no-op для постепенного +> раскатывания, по образцу условного staging-гейта ORCH-35). + +## 4. Изменения схемы БД +- **Не требуется** для базовой реализации (lock через файл/advisory). +- ЕСЛИ архитектор выберет lock через SQLite — добавить таблицу/строку-замок миграцией, + совместимой с текущей инициализацией `src/db.py` (никаких ломающих изменений `tasks`, + `agent_runs`, `jobs`, `events`). Это решение фиксируется в ADR. + +## 5. Изменения API +- Новых HTTP-эндпоинтов **не требуется**. +- Допустимо (не обязательно) расширить `GET /status` или `GET /queue` индикатором + «merge-gate: rebasing/re-testing/locked» для наблюдаемости — на усмотрение архитектора, + без изменения существующих контрактов ответов. + +## 6. Точки встройки в конвейер (требование + кандидаты) + +**Требование:** merge-gate отрабатывает как можно ближе к фактическому слиянию в `main` +и ДО него. Слияние ветки в `main` НЕ должно происходить в обход гейта. + +Кандидаты (окончательно — ADR архитектора): +- **(A)** Гейт на переходе `deploy-staging → deploy` или новый под-гейт перед слиянием: + deployer вливает PR на стадии `deploy`, поэтому проверка «догнать+re-test» логично + встаёт непосредственно перед запуском deployer. +- **(B)** Новая стадия `merge-gate` между `deploy-staging` и `deploy` с агентом=None и + `qg="check_branch_mergeable"`. +- **(C)** Перенести само слияние в `main` из ответственности deployer-агента в + детерминированный шаг оркестратора, защищённый merge-gate (более крупное изменение). + +При любом варианте, меняющем `STAGE_TRANSITIONS` или `QG_CHECKS`: +- обновить `docs/architecture/README.md` (таблица стадий + реестр QG, §«Конвейер»); +- обновить snapshot-тесты `tests/test_qg_registry_snapshot.py` + (`_EXPECTED_QGS`, `_EXPECTED_TRANSITIONS`) — осознанно, в этом же PR; +- сохранить порядок ключей `STAGE_TRANSITIONS` (от него зависит `get_previous_stage`). + +## 7. Откаты (интеграция со `stage_engine`) +В `_handle_qg_failure_rollbacks` добавить ветку для merge-gate FAIL по образцу +`check_staging_status` / `check_deploy_status`: +- `update_task_stage(task_id, "development")`, `set_issue_blocked(work_item_id)`; +- комментарий в Plane (`plane_add_comment`, author="deployer" или системный) с причиной + (конфликт rebase / красный re-test) — дословный `reason` гейта; +- Telegram-алерт (`send_telegram`); +- учитывать `MAX_DEVELOPER_RETRIES`, не плодить бесконечные заворот-циклы. +- В `_run_qg` добавить диспетчеризацию `check_branch_mergeable` с сигнатурой + `(repo, work_item_id, branch)` (как у артефактных чеков). + +## 8. Изменения конфигурации (`src/config.py`, env-префикс `ORCH_`) +| Setting | Назначение | Дефолт (предложение) | +|---------|-----------|----------------------| +| `merge_gate_enabled: bool` | Глобальный вкл/выкл гейта | `True` | +| `merge_retest_timeout_s: int` | Тайм-аут повторного прогона тестов | `600` | +| `merge_lock_timeout_s: int` | Тайм-аут ожидания merge-lock | `300` | +| `merge_gate_repos: str` | (опц.) ограничить гейт списком репо; пусто = все | `""` | + +Значения и имена финализирует архитектор; задокументировать в `.env.example` и +`docs/architecture/README.md`. + +## 9. Требования к наблюдаемости / документации (golden source) +- Обновить `docs/architecture/README.md`: описание merge-gate, auto-rebase, re-test, + merge-lock; при изменении стадий/реестра — соответствующие таблицы. +- Обновить `CHANGELOG.md`. +- Завести ADR `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md` (механизм догона, + выбор rebase vs merge, реализация lock, место встройки). +- Все ветки кода — с лог-сообщениями (`logger.info/warning/error`) по образцу соседних + гейтов, чтобы поведение читалось в `/app/data/runs` и логах сервиса. + +## 10. Нефункциональные требования +- **Безопасность self-hosting:** никогда не push в `main`; force только `--force-with-lease` + по ветке задачи; прод-контейнер `orchestrator` не рестартить/не ронять. +- **Изоляция:** все git-операции — в worktree ветки (`ensure_worktree`), не в общем clone, + чтобы не словить S-4-гонку параллельных задач. +- **Идемпотентность/restart-safe:** lock и гейт корректно ведут себя при рестарте сервиса. +- **Never-raise** контракт у всех новых чеков/парсеров (как в текущем `src/qg/checks.py`). +- **Совместимость:** не менять сигнатуры/поведение существующих QG-чеков и вебхуков. + +## 11. Артефакты pipeline, которые должны быть созданы/обновлены +- `src/merge_gate.py` (новый), изменения в `src/qg/checks.py`, `src/stages.py`, + `src/stage_engine.py`, `src/config.py`, при необходимости `src/git_worktree.py`. +- Новые тесты в `tests/` + обновлённые snapshot-тесты. +- `docs/architecture/README.md`, `CHANGELOG.md`, `.env.example`, + `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`. diff --git a/docs/work-items/ORCH-043/03-acceptance-criteria.md b/docs/work-items/ORCH-043/03-acceptance-criteria.md new file mode 100644 index 0000000..3d990d2 --- /dev/null +++ b/docs/work-items/ORCH-043/03-acceptance-criteria.md @@ -0,0 +1,105 @@ +# 03 — Критерии приёмки (Acceptance Criteria) + +**Work Item:** ORCH-043 — merge-gate + auto-rebase + re-test +**Автор:** Analyst + +Каждый критерий имеет однозначное условие PASS/FAIL. Все критерии должны быть PASS. + +--- + +## AC-1 — Ветка актуальна: гейт пропускает без догона +- **Дано:** ветка содержит последний `origin/main` (не отстаёт). +- **Когда:** выполняется `check_branch_mergeable(repo, work_item_id, branch)`. +- **PASS:** возвращает `(True, ...)` с причиной «up-to-date», auto-rebase НЕ запускается, + ветка не пушится повторно. +- **FAIL:** возвращает `False`, либо выполняет ненужный rebase/push. + +## AC-2 — Ветка отстаёт + чистый догон + зелёный re-test → проход +- **Дано:** ветка отстаёт от `origin/main`; rebase проходит без текстового конфликта; + тест-набор на догнанной ветке зелёный. +- **Когда:** выполняется merge-gate. +- **PASS:** ветка догнана до `origin/main`, запушена `--force-with-lease`, re-test зелёный, + гейт возвращает `(True, ...)`. +- **FAIL:** гейт возвращает `False` при чистом догоне и зелёном re-test, либо `main` тронут, + либо push выполнен НЕ через `--force-with-lease`. + +## AC-3 — Текстовый конфликт rebase → откат на development, без слияния +- **Дано:** auto-rebase упирается в текстовый конфликт. +- **Когда:** выполняется merge-gate. +- **PASS:** rebase отменён (worktree чист), гейт возвращает `(False, "rebase conflict...")`, + задача переведена на `development`, в Plane — комментарий с причиной, слияния в `main` нет. +- **FAIL:** ветка осталась в конфликтном состоянии, или задача продвинулась к слиянию, + или `main` изменён. + +## AC-4 — Красный re-test после догона → откат на development, без слияния +- **Дано:** rebase чистый, но тесты на догнанной ветке падают. +- **Когда:** выполняется merge-gate. +- **PASS:** гейт возвращает `(False, "re-test failed after rebase...")`, задача на + `development`, комментарий в Plane, слияния нет. +- **FAIL:** гейт вернул `True`, либо слияние произошло при красном re-test. + +## AC-5 — Сериализация слияний (merge-lock) +- **Дано:** две задачи одного `repo` одновременно подходят к merge-gate. +- **Когда:** обе пытаются пройти гейт. +- **PASS:** «догон+re-test+слияние» выполняет одновременно только одна задача; вторая + ждёт освобождения lock (в пределах `merge_lock_timeout_s`), после чего повторно + сверяет «не отстаёт» и при необходимости догоняется. Воспроизводимый сценарий + «две зелёные ветки ломают main» НЕ приводит к красному `main`. +- **FAIL:** обе задачи параллельно проходят гейт и вливаются, воспроизводя гонку. + +## AC-6 — Re-test тайм-аут управляем +- **Дано:** re-test превышает `settings.merge_retest_timeout_s`. +- **PASS:** прогон прерывается, гейт возвращает `(False, "re-test timeout...")`, задача + не виснет, идёт штатный откат. +- **FAIL:** задача висит дольше тайм-аута или падает с необработанным исключением. + +## AC-7 — Никогда не push/merge в main напрямую из гейта +- **PASS:** код merge-gate не выполняет `git push ... main` и не форс-пушит `main`; + force-операции — только `--force-with-lease` по ветке задачи. +- **FAIL:** найден любой push/force-push в `main` из логики гейта. + +## AC-8 — Изоляция в worktree +- **PASS:** все git-операции гейта идут в worktree ветки (`get_worktree_path` / + `ensure_worktree`), а не в общем `/repos/` clone. +- **FAIL:** rebase/тесты выполняются в общем clone, создавая S-4-гонку. + +## AC-9 — Контракт never-raise +- **Дано:** недоступен git/сеть, бит worktree, отсутствует ветка и т.п. +- **PASS:** `check_branch_mergeable` и функции `merge_gate.py` возвращают `(False, "")` + (или безопасный фоллбэк), НИКОГДА не пробрасывают исключение в `advance_stage`. +- **FAIL:** любое необработанное исключение всплывает из гейта. + +## AC-10 — Реестр QG и снапшоты консистентны +- **PASS:** `"check_branch_mergeable"` зарегистрирован в `QG_CHECKS` и callable; + `tests/test_qg_registry_snapshot.py` (`_EXPECTED_QGS`, при изменении стадий — + `_EXPECTED_TRANSITIONS`) обновлены и зелёные; порядок ключей `STAGE_TRANSITIONS` + сохранён (не сломан `get_previous_stage`). +- **FAIL:** дрейф реестра/стадий без обновления снапшотов; красные snapshot-тесты. + +## AC-11 — Интеграция отката в stage_engine +- **PASS:** в `_handle_qg_failure_rollbacks` есть ветка merge-gate FAIL → `development` + с уведомлениями (Plane + Telegram) и учётом `MAX_DEVELOPER_RETRIES`; `_run_qg` + корректно диспетчеризует новый чек. +- **FAIL:** FAIL гейта не приводит к откату, или нет уведомления, или зацикливание заворотов. + +## AC-12 — Условный no-op / выключение (если реализовано) +- **Дано:** `settings.merge_gate_enabled = False` (или репо вне `merge_gate_repos`). +- **PASS:** гейт возвращает `(True, "merge-gate disabled")`, конвейер работает как прежде. +- **FAIL:** гейт блокирует/ломает конвейер при выключенном флаге. + +## AC-13 — Документация обновлена (golden source) +- **PASS:** обновлены `docs/architecture/README.md` (merge-gate/auto-rebase/re-test, + при изменении — таблицы стадий/реестра), `CHANGELOG.md`, `.env.example` (новые + `ORCH_*` настройки); создан ADR `06-adr/ADR-001-merge-gate.md`. +- **FAIL:** функционал изменён, документация/ADR/CHANGELOG не обновлены (Reviewer → + REQUEST_CHANGES). + +## AC-14 — Безопасность self-hosting +- **PASS:** в рамках задачи прод-контейнер `orchestrator` (8500) не рестартился и не падал; + изменения не трогают `.env*`, `docker-compose.yml`, прод-инфраструктуру; страховка + стадией `deploy-staging` сохранена. +- **FAIL:** любой рестарт/падение прод-оркестратора или правка прод-инфры в рамках задачи. + +## AC-15 — Зелёный регресс +- **PASS:** `pytest tests/ -q` зелёный целиком (новые тесты ORCH-043 + существующий набор). +- **FAIL:** любой упавший/сломанный существующий тест. diff --git a/docs/work-items/ORCH-043/04-test-plan.yaml b/docs/work-items/ORCH-043/04-test-plan.yaml new file mode 100644 index 0000000..e0ed13b --- /dev/null +++ b/docs/work-items/ORCH-043/04-test-plan.yaml @@ -0,0 +1,163 @@ +work_item: ORCH-043 +title: "merge-gate + auto-rebase + re-test — безопасная параллель в одном репо" +framework: pytest +notes: > + Тесты на git-операции используют локальные временные репозитории (init bare "origin" + + рабочая ветка), мокают сеть/Plane/Telegram (как в tests/test_qg.py: + ORCH_DB_PATH/ORCH_REPOS_DIR в tmp, httpx замокан). Каталог тестов/команда pytest для + re-test должны совпадать с CI-конфигом проекта. Финальные имена функций/модулей сверять + с реализацией архитектора. + +tests: + # ---- merge_gate core: ancestor / behind detection ---- + - id: TC-01 + type: unit + description: "branch_is_behind_main → True, когда origin/main ушёл вперёд относительно ветки" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-02 + type: unit + description: "branch_is_behind_main → False, когда ветка уже содержит весь origin/main" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-03 + type: unit + description: "branch_is_behind_main never-raise: недоступный git/clone → безопасный возврат, не исключение" + module: tests/test_merge_gate.py + expected: PASS + + # ---- auto-rebase ---- + - id: TC-04 + type: unit + description: "auto_rebase_onto_main: чистый догон → (True), ветка содержит origin/main, push выполнен через --force-with-lease" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-05 + type: unit + description: "auto_rebase_onto_main: текстовый конфликт → rebase отменён (worktree чист), (False, 'rebase conflict...'), main не тронут" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-06 + type: unit + description: "auto_rebase_onto_main НЕ пушит и не форс-пушит main ни при каком исходе (проверка вызванных git-команд)" + module: tests/test_merge_gate.py + expected: PASS + + # ---- re-test ---- + - id: TC-07 + type: unit + description: "retest_branch: pytest rc=0 → (True, 're-test green')" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-08 + type: unit + description: "retest_branch: pytest rc!=0 → (False, 're-test failed...') с хвостом вывода" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-09 + type: unit + description: "retest_branch: превышен merge_retest_timeout_s → (False, 're-test timeout...'), без виса" + module: tests/test_merge_gate.py + expected: PASS + + # ---- merge-lock / сериализация ---- + - id: TC-10 + type: unit + description: "merge-lock: второй захват того же repo не проходит, пока lock удержан; освобождается в finally/после ошибки" + module: tests/test_merge_gate.py + expected: PASS + - id: TC-11 + type: unit + description: "merge-lock restart-safe: устаревший/осиротевший lock не блокирует навсегда (тайм-аут merge_lock_timeout_s)" + module: tests/test_merge_gate.py + expected: PASS + + # ---- QG check_branch_mergeable ---- + - id: TC-12 + type: unit + description: "check_branch_mergeable: ветка актуальна → (True, 'up-to-date'), rebase не вызывался" + module: tests/test_qg_merge_gate.py + expected: PASS + - id: TC-13 + type: unit + description: "check_branch_mergeable: отстаёт + чистый rebase + зелёный re-test → (True)" + module: tests/test_qg_merge_gate.py + expected: PASS + - id: TC-14 + type: unit + description: "check_branch_mergeable: конфликт rebase → (False, 'rebase conflict...')" + module: tests/test_qg_merge_gate.py + expected: PASS + - id: TC-15 + type: unit + description: "check_branch_mergeable: красный re-test после догона → (False, 're-test failed after rebase...')" + module: tests/test_qg_merge_gate.py + expected: PASS + - id: TC-16 + type: unit + description: "check_branch_mergeable never-raise: внутренняя ошибка → (False, reason), не исключение; lock освобождён" + module: tests/test_qg_merge_gate.py + expected: PASS + - id: TC-17 + type: unit + description: "merge_gate_enabled=False (или репо вне merge_gate_repos) → (True, 'merge-gate disabled') no-op" + module: tests/test_qg_merge_gate.py + expected: PASS + + # ---- реестр QG / стадии ---- + - id: TC-18 + type: unit + description: "'check_branch_mergeable' присутствует в QG_CHECKS и callable" + module: tests/test_qg_registry_snapshot.py + expected: PASS + - id: TC-19 + type: unit + description: "snapshot STAGE_TRANSITIONS/_EXPECTED_QGS обновлён осознанно и совпадает; порядок ключей сохранён (get_previous_stage не сломан)" + module: tests/test_qg_registry_snapshot.py + expected: PASS + + # ---- интеграция со stage_engine (откаты) ---- + - id: TC-20 + type: integration + description: "_run_qg диспетчеризует check_branch_mergeable с сигнатурой (repo, work_item_id, branch)" + module: tests/test_stage_engine.py + expected: PASS + - id: TC-21 + type: integration + description: "merge-gate FAIL → advance_stage откатывает задачу на 'development', set_issue_blocked, комментарий Plane, Telegram-алерт (моки)" + module: tests/test_stage_engine.py + expected: PASS + - id: TC-22 + type: integration + description: "merge-gate FAIL уважает MAX_DEVELOPER_RETRIES — нет бесконечного цикла заворотов" + module: tests/test_stage_engine.py + expected: PASS + - id: TC-23 + type: integration + description: "merge-gate PASS → задача продвигается к слиянию/деплою, рассинхрона стадий нет" + module: tests/test_stage_engine.py + expected: PASS + + # ---- сквозной сценарий гонки ---- + - id: TC-24 + type: integration + description: > + Воспроизведение бизнес-сценария: A и B от main@C0; A влита (main@C1); + B проходит merge-gate → догоняется до C1 и re-test зелёный → безопасное слияние; + при красном re-test B откатывается, main остаётся зелёным + module: tests/test_merge_gate_race.py + expected: PASS + + # ---- конфигурация ---- + - id: TC-25 + type: unit + description: "Новые ORCH_* настройки (merge_gate_enabled, merge_retest_timeout_s, merge_lock_timeout_s, merge_gate_repos) читаются с дефолтами и env-override" + module: tests/test_config.py + expected: PASS + + # ---- регресс ---- + - id: TC-26 + type: integration + description: "Полный набор pytest tests/ -q зелёный (существующие гейты/вебхуки/стадии не сломаны)" + module: tests/ + expected: PASS