From 3a2a5063e0c3e8ff8b8fd5a3d0be4eab4a48dc8d Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 15 Jun 2026 09:22:15 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=686 --- docs/work-items/ORCH-110/01-brd.md | 178 ++++++++++++++++++ docs/work-items/ORCH-110/02-trz.md | 129 +++++++++++++ .../ORCH-110/03-acceptance-criteria.md | 123 ++++++++++++ docs/work-items/ORCH-110/04-test-plan.yaml | 94 +++++++++ 4 files changed, 524 insertions(+) create mode 100644 docs/work-items/ORCH-110/01-brd.md create mode 100644 docs/work-items/ORCH-110/02-trz.md create mode 100644 docs/work-items/ORCH-110/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-110/04-test-plan.yaml diff --git a/docs/work-items/ORCH-110/01-brd.md b/docs/work-items/ORCH-110/01-brd.md new file mode 100644 index 0000000..0f045c0 --- /dev/null +++ b/docs/work-items/ORCH-110/01-brd.md @@ -0,0 +1,178 @@ +--- +work_item: ORCH-110 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 01 — BRD (бизнес-требования): ORCH-110 — BUG: merge-gate local re-test timeout causes false rollback after green CI + +Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis + +> **Багфикс-трек → эскалация в полный цикл (`escalate: full-cycle`).** Задача помечена `Bug`, но +> сама баг-карточка требует «отдельный анализ вариантов и контрактов merge-gate» (см. «Ограничение» +> ниже) — это решение с несколькими проектными альтернативами и нетривиальными инвариантами +> self-hosting, которому нужен ADR. По правилу ORCH-019 (ADR-001 D5) выпускается **полный** +> analysis-пакет, а трек эскалируется (`POST /bug-fast-track/escalate?work_item=ORCH-110`) → задача +> проходит стадию `architecture`. Прецедент — родственная задача ORCH-111 («bug → escalate +> full-cycle»). + +--- + +## 1. Бизнес-контекст и проблема + +Оркестратор — self-hosting инструмент: его прод-контейнер обслуживает конвейер всех проектов и +дорабатывает сам себя. На ребре `deploy-staging → deploy` исполняется детерминированный под-гейт +**merge-gate** (`check_branch_mergeable`, ORCH-043): он догоняет ветку до текущего `origin/main` +(`auto_rebase_onto_main`) и затем **локально пере-прогоняет весь тест-сюит** (`retest_branch` → +`python -m pytest tests/ -q`) в worktree, чтобы поймать **семантический** конфликт слияния (ветка +зелёная по своей базе, но ломает уехавший `main`). + +**Установленные факты инцидента (ORCH-109, PR #129):** +- tester завершился `result: PASS`; полный регресс — **`1899 passed` за `516.70s`**; +- CI Gitea по HEAD — зелёный (push + pull_request success); PR после rebase — open, `mergeable=true`; +- merge-gate локальный re-test упал по **таймауту**: `re-test timeout after 600s` + (`merge_retest_timeout_s = 600`); +- на хосте обнаружены **старые зависшие pytest-процессы** `tests/test_install_lite_script.py`, + жившие **> 2 суток** и грузившие CPU; прибиты вручную 2026-06-14. + +**Цепочка отказа.** Зависшие осиротевшие pytest-процессы (CPU-голодание) → тот же сюит, что у tester +шёл 516.70s (запас до 600s ≈ 16%), под нагрузкой превысил 600s → `check_branch_mergeable` вернул +`(False, "re-test timeout after 600s")` → `_handle_merge_gate_rollback`: откат `deploy-staging → +development` + developer-retry. Каждый из 3 retry повторно падал по тому же CPU-голоданию → финальный +alert **«Merge-gate still failing after 3 developer retries (re-test timeout after 600s)»** → задача +застряла, потребовалось ручное вмешательство. + +**Корень (подтверждён по коду):** +1. **Утечка осиротевших процессов.** `merge_gate.retest_branch` и `coverage_gate.measure_coverage` + запускают `subprocess.run([... pytest ...], timeout=...)` **без изоляции группы процессов** + (`start_new_session`/`preexec_fn`). При `TimeoutExpired` Python убивает только **прямого + потомка**; внуки pytest репарентируются на PID 1 (tini жнёт зомби, но не убивает живых сирот) и + живут сутками, грузя CPU. Это источник CPU-голодания (ровно симптом из фактов). +2. **Нет толерантности к инфра-таймауту.** Re-test **таймаут** (ресурсная/инфра-причина) + классифицируется идентично **красному** re-test (реальный дефект кода): оба → откат на + `development` + расход developer-retry. Разработчик не может «починить» CPU-голодание → retry + сгорают вхолостую и упираются в alert «Manual intervention needed». +3. **Тонкий бюджет.** Бюджет re-test `600s` практически равен фактическому времени сюита + (`516.70s`); запас не растёт вместе с сюитом (ср. ORCH-109, где по той же причине были подняты + бюджеты агентов developer/reviewer). +4. **Контракт необходимости re-test.** На ветке, уже актуальной к `origin/main` (rebase — no-op), и + с зелёным CI по этому же HEAD локальный полный re-test пере-проверяет ровно тот коммит, что CI + уже подтвердил, — становясь избыточной единственной точкой ложного отказа. + +## 2. Объём (scope) + +### В объёме +- Поведение merge-gate при **таймауте** локального re-test: классификация и путь восстановления + (толерантность к инфра-таймауту вместо ложного отката на `development`). +- **Жизненный цикл подпроцессов**, которые оркестратор запускает САМ для проверок: re-test merge-gate + (`merge_gate.retest_branch`) и coverage-run (`coverage_gate.measure_coverage`) — гарантия отсутствия + осиротевших процессов после таймаута/kill. +- **Согласованность бюджета** re-test с фактическим временем полного сюита (адекватный запас) с учётом + сквозных инвариантов reaper/lease. +- **Контракт необходимости** локального re-test merge-gate (когда он реально нужен относительно + зелёного CI и состояния `branch vs origin/main`) — анализ вариантов под решение архитектора. +- Наблюдаемость инфра-таймаута (отличить «инфра-таймаут, повтор/defer» от «дефект кода → developer»). + +### Вне объёма +- **Алерт sidecar-watchdog на осиротевший тест-процесс** — это **ORCH-111** (`proc_blocking`, + наблюдатель только сигналит, никогда не убивает, C-1). ORCH-110 — комплементарная сторона + (предотвращение утечки + толерантность), дубля детекции не вводит. +- Ручное умерщвление уже зависших хост-процессов — операционная мера (выполнена 2026-06-14), не код. +- Любые правки `STAGE_TRANSITIONS` / реестра `QG_CHECKS` / `check_*`-семантики / machine-verdict + ключей / схемы БД (инвариант NFR-1). +- Изменение конкретного теста `tests/test_install_lite_script.py` (его поведение — отдельный предмет; + здесь важен класс «оркестратор-спавненный pytest не должен переживать свой бюджет»). +- Поведение не-self-hosting репозиториев (enduro-trails) — нулевая регрессия. +- Изменение хука прод-деплоя/рестарт прод-контейнера (self-hosting безопасность). + +## 3. Заинтересованные стороны +- **Owner / оператор self-hosting** — страдает от ручного разбора застрявших задач и зависших + процессов; заказчик исправления. +- **Конвейер всех проектов** — общий прод-контейнер: утечка CPU деградирует обслуживание enduro. +- **Пакетный автономный режим (эпик ORCH-088)** — ложные откаты и manual-gate'ы ломают цель + «10–20 задач за ночь без вмешательства». +- **Принимает результат:** reviewer → tester → deployer штатного конвейера. + +## 4. Бизнес-требования (BR) + +- **BR-1 — Зелёный путь без ручного вмешательства.** При зелёном tester `PASS` и зелёном CI задача + **не должна** требовать ручного вмешательства из-за инфраструктурного/локального re-test таймаута + (прямое «Ожидаемое поведение» баг-карточки). +- **BR-2 — Инфра-таймаут ≠ дефект кода.** Таймаут локального re-test merge-gate (ресурсная/инфра + причина) **не должен** трактоваться как код-фейл: путь восстановления **не** должен сжигать + developer-retry и приводить к «Manual intervention after N developer retries», если CI и tester + были зелёными. Реакция на таймаут — ограниченный повтор/defer и/или отдельный инфра-сигнал, не + безусловный откат на `development`. +- **BR-3 — Нет осиротевших процессов.** Подпроцессы pytest, запущенные самим оркестратором для + re-test и coverage-run, **должны** полностью завершаться (всё дерево, включая внуков) при + таймауте/kill. Ни один оркестратор-спавненный pytest не должен переживать свой бюджет и продолжать + грузить CPU. +- **BR-4 — Адекватный бюджет re-test.** Бюджет времени re-test **должен** иметь достаточный запас над + фактическим временем полного сюита, чтобы здоровый сюит при штатной нагрузке не падал по таймауту; + бюджет конфигурируем и со-эволюционирует с ростом сюита. +- **BR-5 — Контракт необходимости re-test.** Merge-gate **должен** различать «ветка реально отстала + от уехавшего `origin/main` и была ребейзнута» (риск семантического конфликта → re-test оправдан) и + «ветка уже актуальна / rebase — no-op, CI по этому HEAD зелёный» (re-test избыточен). Локальный + re-test не должен быть избыточной единственной точкой ложного отказа на коммите, уже подтверждённом + CI. Конкретный контракт (skip/scope/trust-CI-SHA) выбирает архитектор и фиксирует в ADR. +- **BR-6 — Сохранение защиты от семантического конфликта.** Толерантность к таймауту **не должна** + ослаблять исходную цель merge-gate (ORCH-043): **детерминированно красный** re-test (реальный сбой + теста, а не таймаут) по-прежнему обязан откатывать на `development`. Послабление применяется ТОЛЬКО + к таймауту/инфра, никогда к красному результату. +- **BR-7 — Наблюдаемость.** Состояние «инфра-таймаут» должно быть видимым (лог + Telegram с + кликабельным номером + read-only в `GET /queue`) и отличимым от код-фейл-отката; согласовано с + сигналом ORCH-111 (без дубля). + +## 5. Нефункциональные требования (NFR) + +- **NFR-1 — Инварианты конвейера неприкосновенны.** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / + семантика `check_*` / machine-verdict ключи (`verdict:`/`result:`/`deploy_status:`/ + `staging_status:`/`security_status:`/`coverage_status:`) / схема БД — **байт-в-байт** прежние. + Исправление — аддитивное (врезка/leaf-логика), не новая стадия и не новый зарегистрированный QG. +- **NFR-2 — Kill-switch + нулевая регрессия.** Новое поведение под флагом; при выключенном флаге — + поведение **байт-в-байт** как до ORCH-110 (таймаут → прежний откат). Скоуп — self-hosting + (`orchestrator`); enduro не затронут. +- **NFR-3 — Self-hosting безопасность.** Исправление **никогда** не пушит/force-push в `main` (INV-4; + merge только через Gitea PR-merge API), не рестартит прод-контейнер, не трогает detached-деплой. +- **NFR-4 — never-raise.** Любая ошибка в новом пути → безопасный дефолт + WARNING; исключение + никогда не уходит в `advance_stage`/monitor-поток (контракт merge-gate сохранён). +- **NFR-5 — Ограниченность (anti-loop).** Любой повтор/defer таймаута строго ограничен по числу + попыток и суммарному времени; исчерпание → **чёткий инфра-alert**, отличный от «developer must + fix», а не бесконечный bounce и не молчаливое зависание. +- **NFR-6 — Сквозные инварианты времени.** Любое изменение бюджета re-test должно уважать + существующие соотношения: `merge_lock_timeout_s` (TTL merge-lease), `reaper_max_running_s` + (Tier-3 backstop reaper, ORCH-065/109), `coverage_run_timeout_s` — без рассинхрона. + +## 6. Допущения и ограничения +- **Ограничение из баг-карточки (дословно):** «Решение намеренно не описано в этой баге; нужен + отдельный анализ вариантов и контрактов merge-gate». → Аналитик фиксирует требования и + тест-план; **варианты и контракт merge-gate** прорабатывает архитектор (06-adr) — основание + эскалации в полный цикл. +- Допущение: tini (PID 1) жнёт зомби, но не убивает живых осиротевших процессов (подтверждено + поведением инцидента) — отсюда требование tree-kill (BR-3). +- Допущение: таймаут merge-gate re-test в зелёном инциденте вызван внешним CPU-голоданием, а не + реальным зависанием теста ветки; но решение обязано остаться **fail-safe** к случаю реального + зависшего теста (см. Риск R-2 / BR-6). +- Среда верификации — staging-контур (8501), обязательная страховка перед прод-деплоем self. + +## 7. Критерии успеха +Резюме: зелёный tester `PASS` + зелёный CI + актуальная ветка → задача доходит до `deploy` без +ложного отката на `development` и без manual-gate из-за инфра-таймаута; оркестратор-спавненные +pytest-процессы не переживают свой бюджет; реальный красный re-test по-прежнему откатывает на +`development`; инварианты конвейера и self-hosting не тронуты. Детальные PASS/FAIL — +`03-acceptance-criteria.md`. + +## 8. Риски +- **R-1** — Над-толерантность маскирует реальный зависший тест (бесконечный/долгий) как «инфра» → + смягчение: строгая ограниченность (NFR-5) + отдельный инфра-alert + сохранение красно-откат-пути + (BR-6). +- **R-2** — Поднятие бюджета без правки tree-kill лишь отодвигает отказ (сюит растёт) → исправление + должно бить корень (BR-3), бюджет (BR-4) — вторично. +- **R-3** — Рассинхрон сквозных таймаутов (reaper/lease) при изменении бюджета (NFR-6). +- **R-4** — Дубль/конфликт с сигналом ORCH-111 (`proc_blocking`) → координация: ORCH-110 + предотвращает/толерирует, ORCH-111 наблюдает; разные слои. +- Детальная оценка и митигации — `10-tech-risks.md` (заполняет архитектор). diff --git a/docs/work-items/ORCH-110/02-trz.md b/docs/work-items/ORCH-110/02-trz.md new file mode 100644 index 0000000..f66d631 --- /dev/null +++ b/docs/work-items/ORCH-110/02-trz.md @@ -0,0 +1,129 @@ +--- +work_item: ORCH-110 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 02 — ТЗ (TRZ): ORCH-110 — merge-gate local re-test timeout: устранение ложного отката + утечки процессов + +Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные требования к реализации**, выведенные из BRD и фактического кода. +> Архитектурное обоснование, выбор вариантов и **контракт merge-gate** — задача архитектора (06-adr, +> основание `escalate: full-cycle`). Здесь — поведение/контракты/инварианты и привязка к модулям, +> НЕ выбор механизма. + +## 1. Сводка изменения +Устранить ложный откат `deploy-staging → development`, возникающий когда локальный re-test merge-gate +падает по **таймауту** (инфра/ресурс), при зелёном tester `PASS` и зелёном CI. Изменение бьёт по двум +корням и одному контракту: (1) **утечка осиротевших pytest-процессов** из оркестратор-спавненных +прогонов re-test/coverage (источник CPU-голодания) → гарантировать tree-kill дерева подпроцесса при +таймауте/kill; (2) **классификация инфра-таймаута** как транзиента (повтор/defer/инфра-alert), а не +код-фейла (откат + расход developer-retry); (3) **контракт необходимости** локального re-test +относительно зелёного CI и состояния `branch vs origin/main`. Сопутствующе — согласование **бюджета** +re-test с реальным временем сюита. Всё — аддитивно, под kill-switch, never-raise, скоуп self-hosting, +с сохранением исходной защиты merge-gate от семантического конфликта (красный re-test по-прежнему +откатывает). + +## 2. Задействованные модули / пути +| Путь | Действие | +|------|----------| +| `src/merge_gate.py` | изменить — `retest_branch`: жизненный цикл подпроцесса (tree-kill при таймауте/kill); классификация исхода «timeout» как транзиента (контракт возврата) | +| `src/coverage_gate.py` | изменить — `measure_coverage`: тот же tree-kill при таймауте (сиблинг-источник утечки, BR-3) | +| `src/qg/checks.py` | изменить — `check_branch_mergeable`: различать «timeout/infra» от «red re-test» в возвращаемом контракте (без смены имени/семантики зарегистрированного `check_*`) | +| `src/stage_engine.py` | изменить — `_handle_merge_gate` / маршрутизация исхода: инфра-таймаут → defer/повтор/инфра-alert (по образцу `_handle_merge_gate_defer`), НЕ `_handle_merge_gate_rollback`; красный re-test → прежний rollback | +| `src/config.py` | изменить — флаг(и) толерантности к инфра-таймауту + (опц.) согласование `merge_retest_timeout_s`; уважить сквозные инварианты `merge_lock_timeout_s` / `reaper_max_running_s` / `coverage_run_timeout_s` | +| `docs/architecture/README.md`, `CLAUDE.md`, `CHANGELOG.md` | обновить — описание поведения merge-gate re-test (golden source наравне с кодом) | +| `tests/test_*` | создать — покрытие по `04-test-plan.yaml` | + +> Точный набор новых символов/флагов и механизм tree-kill (process-group `start_new_session`+killpg, +> либо иной) — решение архитектора. ТЗ фиксирует **что** должно выполняться, не **как**. + +## 3. Функциональные требования + +### FR-1 — Толерантность к инфра-таймауту re-test (нет ложного отката) [BR-1, BR-2] +Когда merge-gate локальный re-test завершается специфически по **таймауту** (а не детерминированно +красным результатом), исход ДОЛЖЕН классифицироваться как транзиент/инфра, не код-фейл. Путь +восстановления НЕ ДОЛЖЕН быть тем же `_handle_merge_gate_rollback` (откат на `development` + инкремент +developer-retry), который при зелёных CI/tester ведёт к «Manual intervention needed». Допустимая +реакция (выбор — архитектор): ограниченный повтор re-test и/или defer (по образцу существующего +`_handle_merge_gate_defer` для `merge-lock busy`) и/или отдельный инфра-alert. Прецеденты толерантности +к инфра: ORCH-061 (staging infra tolerance), ORCH-093 (transient vs terminal классификация merge-POST). + +### FR-2 — Tree-kill оркестратор-спавненных тест-процессов [BR-3] +`merge_gate.retest_branch` и `coverage_gate.measure_coverage` ДОЛЖНЫ гарантировать, что при таймауте +(а также при любом kill/прерывании прогона) завершается **всё дерево** подпроцесса pytest, включая +внуков, а не только прямой потомок. После таймаута ни один оркестратор-спавненный pytest-процесс не +должен оставаться живым и грузить CPU. Контракт возврата `retest_branch` +(`(False, "re-test timeout after s")`) сохраняется; меняется лишь побочный эффект — отсутствие +утечки. Существующий каскад launcher `SIGTERM→grace→SIGKILL` (`stop_process`) — образец на уровне +агентов; для этих subprocess-прогонов требуется эквивалентная гарантия на уровне группы процессов. + +### FR-3 — Согласованность бюджета re-test [BR-4, NFR-6] +Бюджет `merge_retest_timeout_s` ДОЛЖЕН иметь достаточный запас над фактическим временем полного сюита +(наблюдаемо: 600s бюджет vs 516.70s факт ≈ 16%). Бюджет остаётся конфигурируемым; при его изменении +ДОЛЖНЫ соблюдаться сквозные инварианты: `reaper_max_running_s > max(agent_timeout, бюджеты) + grace` +(ORCH-065/109) и согласование с `merge_lock_timeout_s` (TTL merge-lease держится на время re-test). +Малформный/непозитивный конфиг → безопасный дефолт + WARNING (never-break). + +### FR-4 — Контракт необходимости локального re-test [BR-5, BR-6] +Merge-gate ДОЛЖЕН различать риск-кейсы и применять re-валидацию пропорционально реальному риску +слияния: +- ветка **реально отстала** от уехавшего `origin/main` и ребейзнута → семантический риск → re-test + оправдан (текущая цель ORCH-043 сохраняется); +- ветка **уже актуальна** / rebase — no-op, и CI по этому самому HEAD зелёный → локальный полный + re-test пере-проверяет ровно подтверждённый CI коммит и не должен быть единственной точкой ложного + отказа. +Конкретный контракт (например: пропуск re-test при «не-behind + зелёный CI по HEAD», сокращённый +scope, доверие SHA, подтверждённому CI, и т. п.) — **выбор архитектора в ADR** (ядро запрошенного +баг-карточкой «анализа контрактов merge-gate»). Инвариант **BR-6**: детерминированно **красный** +re-test (реальный сбой теста) обязан и далее откатывать на `development` — послабление применяется +ТОЛЬКО к таймауту/инфра. + +### FR-5 — Сохранение инвариантов и kill-switch [NFR-1, NFR-2, NFR-3, NFR-4] +Изменение аддитивно: `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика `check_*` / machine-verdict +ключи / схема БД — без изменений; merge-gate остаётся под-гейтом-врезкой, не новой стадией/QG. Под +kill-switch: выключенный флаг → байт-в-байт прежнее поведение (таймаут → откат). Скоуп self-hosting +(`orchestrator`); enduro — no-op. never-raise; INV-4 (никогда push/force-push `main`; merge только +через Gitea PR API) и запрет рестарта прод-контейнера — соблюдены. + +### FR-6 — Наблюдаемость и ограниченность [BR-7, NFR-5] +Состояние «инфра-таймаут» ДОЛЖНО логироваться, уведомляться в Telegram (кликабельный номер задачи) и +быть видимым read-only (например, расширение блока `merge`/`merge_verify` в `GET /queue`), отличимо от +код-фейл-отката. Любой повтор/defer строго ограничен (число попыток + суммарное время); исчерпание → +**инфра-alert** (не «developer must fix»). Координация с ORCH-111 (`proc_blocking`) — без дубля: +ORCH-110 предотвращает/толерирует, ORCH-111 наблюдает. + +## 4. Изменения API +Новых обязательных эндпоинтов **не требуется**. Допустимо (when-applicable, на усмотрение +архитектора) **read-only** расширение существующего снимка `GET /queue` (блок merge-gate) полями +наблюдаемости инфра-таймаута/повторов. Никаких новых управляющих эндпоинтов. + +## 5. Изменения схемы БД +**Нет.** Счётчики повторов/defer — по образцу существующих (`_merge_defer_count` / +`_developer_retry_count` поверх `jobs`/`agent_runs`) либо in-memory/sentinel; новые таблицы/колонки не +вводятся (NFR-1). + +## 6. Требования к новым/изменённым QG checks +**Нет нового зарегистрированного QG.** `check_branch_mergeable` остаётся в реестре `QG_CHECKS` с тем же +именем и семантикой PASS/FAIL; меняется лишь **различение причины FAIL** (timeout/infra vs red) в +возвращаемом reason и **маршрутизация исхода** во врезке `_handle_merge_gate` (`advance_stage`). +`STAGE_TRANSITIONS` и состав `QG_CHECKS` — байт-в-байт. + +## 7. Совместимость / регресс +- **Обратная совместимость:** kill-switch off → поведение байт-в-байт как до ORCH-110 (таймаут → + rollback на `development`), включая текст alert'ов. +- **Область раската:** self-hosting `orchestrator` (как ORCH-035/043/058/071); прочие репо — no-op, + путь LLM-`deployer`/прежний merge не затронут. +- **Обратимость:** чисто аддитивная логика под флагом; откат = выключить флаг. +- **Self-hosting:** без рестарта прод-контейнера; merge только через Gitea PR API; никаких операций с + `main` (INV-4). +- **Анти-регресс целей merge-gate:** красный re-test → прежний rollback (BR-6); защита от + семантического конфликта/фантомного merge (ORCH-043/071/073) — не ослаблена. +- **Трассировка маркеров (ORCH-078):** правки в `merge_gate.py`/`coverage_gate.py`/`qg/checks.py` + затрагивают блоки с маркерами ORCH-043/071/073/093/027/065/109 — перед изменением сверить их + `06-adr` и не сломать зафиксированные инварианты (lease, never-raise, fail-open/closed, бюджеты). diff --git a/docs/work-items/ORCH-110/03-acceptance-criteria.md b/docs/work-items/ORCH-110/03-acceptance-criteria.md new file mode 100644 index 0000000..d0b49e8 --- /dev/null +++ b/docs/work-items/ORCH-110/03-acceptance-criteria.md @@ -0,0 +1,123 @@ +--- +work_item: ORCH-110 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +escalate: full-cycle +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-110 — merge-gate re-test timeout + +Work Item: **ORCH-110** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что считается +провалом). Reviewer/тестировщик проверяет их буквально по файлам/тестам репозитория. + +--- + +## AC-1 — Зелёный путь не требует ручного вмешательства из-за инфра-таймаута +**Условие:** tester `PASS` + зелёный CI + ветка актуальна к `origin/main`, но локальный re-test +merge-gate упирается в таймаут (смоделированное CPU-голодание/медленный прогон). +- **PASS:** задача НЕ откатывается ложно как код-фейл и НЕ доходит до alert «Merge-gate still failing + after N developer retries»; она доходит до `deploy` (возможно после ограниченного defer/повтора) + или поднимает **отдельный инфра-alert**, отличимый от «developer must fix». +- **FAIL:** таймаут → `_handle_merge_gate_rollback` на `development` с расходом developer-retry → + manual-gate (текущее ошибочное поведение). + +--- + +## AC-2 — Инфра-таймаут классифицируется отдельно от красного re-test +**Условие:** `check_branch_mergeable` вернул FAIL по причине таймаута vs по причине красного теста. +- **PASS:** таймаут маршрутизируется в транзиент-путь (defer/повтор/инфра-alert), красный re-test — + в прежний rollback на `development`; пути различимы в коде и в логах/наблюдаемости. +- **FAIL:** оба исхода идут одним путём отката на `development`. + +--- + +## AC-3 — Реальный красный re-test по-прежнему откатывает (анти-над-толерантность) +**Условие:** локальный re-test даёт детерминированно красный результат (реальный сбой теста, не +таймаут). +- **PASS:** задача откатывается на `development` + developer-retry (цель ORCH-043 сохранена), lease + освобождается. +- **FAIL:** красный re-test «толерируется» и задача продвигается/деплоится со сломанным кодом. + +--- + +## AC-4 — Нет осиротевших тест-процессов после таймаута +**Условие:** `merge_gate.retest_branch` (и `coverage_gate.measure_coverage`) запущены против прогона, +порождающего дочерние/внучатые процессы, и прогон превышает бюджет (таймаут). +- **PASS:** по таймауту завершается всё дерево подпроцесса (включая внуков); живых + оркестратор-спавненных pytest-процессов не остаётся; контракт возврата + `(False, "re-test timeout after s")` сохранён. +- **FAIL:** прямой потомок убит, но внуки репарентируются и продолжают жить/грузить CPU. + +--- + +## AC-5 — Бюджет re-test согласован и уважает сквозные инварианты +**Условие:** конфигурация бюджета re-test и связанных таймаутов. +- **PASS:** `merge_retest_timeout_s` имеет адекватный запас над фактическим временем сюита; + соблюдено `reaper_max_running_s > max(agent_timeout, бюджеты) + grace` и согласование с + `merge_lock_timeout_s`/`coverage_run_timeout_s`; малформный конфиг → дефолт + WARNING. +- **FAIL:** бюджет оставлен впритык к времени сюита без обоснования, либо изменение бюджета ломает + инвариант reaper/lease. + +--- + +## AC-6 — Контракт необходимости re-test зафиксирован и реализован +**Условие:** ветка не-behind (rebase no-op) + зелёный CI по этому HEAD. +- **PASS:** поведение локального re-test соответствует контракту, выбранному архитектором в ADR + (skip/scope/trust-CI-SHA и т. п.); re-test не является избыточной единственной точкой ложного + отказа на коммите, уже подтверждённом CI; решение и его обоснование задокументированы в `06-adr/`. +- **FAIL:** контракт не определён/не реализован; полный re-test безусловно гоняется и при не-behind + + зелёном CI. + +--- + +## AC-7 — Kill-switch и нулевая регрессия +**Условие:** флаг толерантности выключен; и/или не-self-hosting репозиторий (enduro). +- **PASS:** поведение байт-в-байт как до ORCH-110 (таймаут → прежний rollback; те же тексты + alert'ов); enduro-путь не затронут. +- **FAIL:** при выключенном флаге/для enduro поведение изменилось. + +--- + +## AC-8 — Инварианты конвейера и self-hosting не тронуты +**Условие:** статический и поведенческий анализ изменений. +- **PASS:** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика `check_*` / machine-verdict ключи / + схема БД — без изменений; никаких push/force-push в `main` (INV-4), merge только через Gitea PR API, + прод-контейнер не рестартится; все публичные функции merge-gate/coverage остаются never-raise. +- **FAIL:** изменён любой из перечисленных инвариантов, или исключение уходит в `advance_stage`. + +--- + +## AC-9 — Ограниченность транзиент-пути (anti-loop) + наблюдаемость +**Условие:** инфра-таймаут повторяется. +- **PASS:** число повторов/defer ограничено и по попыткам, и по суммарному времени; исчерпание → + один чёткий инфра-alert; событие видно в логах/`GET /queue` и отличимо от код-фейла. +- **FAIL:** бесконечный bounce/повтор, либо молчаливое зависание без терминального сигнала. + +--- + +## AC-10 — Регресс-тест (красный до фикса, зелёный после) +**Условие:** наличие автоматического теста, воспроизводящего инцидент. +- **PASS:** в `tests/` есть тест, который на текущем коде **падал бы** (инфра-таймаут re-test → + ложный rollback / выживший процесс), а после фикса **зелёный**; включён в полный регресс `pytest`. +- **FAIL:** регресс-теста нет, либо он не воспроизводит инцидент. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1 | +| AC-2 | BR-2 / FR-1 | +| AC-3 | BR-6 / FR-4 | +| AC-4 | BR-3 / FR-2 | +| AC-5 | BR-4 / FR-3 / NFR-6 | +| AC-6 | BR-5 / FR-4 | +| AC-7 | NFR-2 / FR-5 | +| AC-8 | NFR-1 / NFR-3 / NFR-4 / FR-5 | +| AC-9 | NFR-5 / BR-7 / FR-6 | +| AC-10 | BR-1…BR-3 (регресс инцидента) | diff --git a/docs/work-items/ORCH-110/04-test-plan.yaml b/docs/work-items/ORCH-110/04-test-plan.yaml new file mode 100644 index 0000000..9b79cd5 --- /dev/null +++ b/docs/work-items/ORCH-110/04-test-plan.yaml @@ -0,0 +1,94 @@ +work_item: ORCH-110 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +escalate: full-cycle +title: "merge-gate re-test timeout: tolerance + no orphan-process leak + re-test contract" +framework: pytest +scope: > + Покрывает: классификацию инфра-таймаута re-test merge-gate как транзиента (без ложного отката), + tree-kill оркестратор-спавненных pytest-прогонов (re-test + coverage), сохранение красно-откат-пути, + согласование бюджета re-test со сквозными инвариантами, контракт необходимости re-test, kill-switch + и наблюдаемость. Вне покрытия: алерт sidecar-watchdog (ORCH-111), правка конкретного + test_install_lite_script.py, поведение enduro (только проверяется no-op). +notes: > + TC-10 — ОБЯЗАТЕЛЬНЫЙ регресс-тест инцидента ORCH-109/PR#129: красный на текущем коде (инфра-таймаут + re-test → ложный rollback / выживший процесс), зелёный после фикса. Подпроцессы в тестах мокаются + (управляемый "медленный/спавнящий детей pytest"), без обращения к сети/Plane/Gitea. Полный регресс + tests/ должен оставаться зелёным. Точные имена символов/флагов уточняет архитектор (06-adr); + модули-плейсхолдеры ниже выровнены под манифест PIPELINE_DOCS. + +tests: + - id: TC-01 + type: unit + description: "retest_branch: таймаут возвращает (False, 're-test timeout after s') И завершает всё дерево подпроцесса (внуки не переживают таймаут)." + module: tests/test_orch110_retest_lifecycle.py + expected: PASS + + - id: TC-02 + type: unit + description: "coverage_gate.measure_coverage: таймаут завершает всё дерево подпроцесса (сиблинг-источник утечки, BR-3); возврат None сохранён." + module: tests/test_orch110_retest_lifecycle.py + expected: PASS + + - id: TC-03 + type: unit + description: "check_branch_mergeable: исход 'timeout' различим от 'red re-test' в reason/классификации (без смены имени/семантики зарегистрированного check_*)." + module: tests/test_orch110_merge_gate_classify.py + expected: PASS + + - id: TC-04 + type: unit + description: "Маршрутизация исхода: инфра-таймаут → defer/повтор/инфра-alert путь (НЕ _handle_merge_gate_rollback на development, без инкремента developer-retry)." + module: tests/test_orch110_merge_gate_routing.py + expected: PASS + + - id: TC-05 + type: unit + description: "Анти-над-толерантность: детерминированно КРАСНЫЙ re-test по-прежнему → откат на development + developer-retry + release lease (BR-6 сохранён)." + module: tests/test_orch110_merge_gate_routing.py + expected: PASS + + - id: TC-06 + type: unit + description: "Ограниченность (anti-loop): повторы/defer инфра-таймаута лимитированы по попыткам и суммарному времени; исчерпание → один инфра-alert, не бесконечный bounce." + module: tests/test_orch110_merge_gate_routing.py + expected: PASS + + - id: TC-07 + type: unit + description: "Kill-switch off → байт-в-байт прежнее поведение (таймаут → rollback на development, прежний текст alert); not-self repo (enduro) → no-op." + module: tests/test_orch110_killswitch.py + expected: PASS + + - id: TC-08 + type: unit + description: "Бюджет/инварианты: malformed/непозитивный merge_retest_timeout_s → дефолт + WARNING; соблюдён reaper_max_running_s > max(agent_timeout, бюджеты)+grace и согласование с merge_lock_timeout_s." + module: tests/test_orch110_budget_invariants.py + expected: PASS + + - id: TC-09 + type: unit + description: "never-raise: любая ошибка в новом транзиент-пути → безопасный дефолт + WARNING; исключение не уходит в advance_stage." + module: tests/test_orch110_merge_gate_routing.py + expected: PASS + + - id: TC-10 + type: integration + description: "РЕГРЕСС инцидента: tester PASS + зелёный CI + ветка не-behind, но re-test таймаут — задача НЕ откатывается ложно и НЕ упирается в 'Merge-gate still failing after N developer retries'; доходит до deploy или поднимает инфра-alert. Красный до фикса, зелёный после." + module: tests/test_orch110_false_rollback_regression.py + expected: PASS + + - id: TC-11 + type: integration + description: "Контракт необходимости re-test (FR-4/AC-6): при не-behind + зелёном CI по HEAD локальный re-test ведёт себя по выбранному в ADR контракту (skip/scope/trust-CI) и не является избыточной единственной точкой ложного отказа. Финальная форма — после решения архитектора." + module: tests/test_orch110_retest_contract.py + expected: PASS + + - id: TC-12 + type: integration + description: "Наблюдаемость: инфра-таймаут отражён в логах/GET /queue (read-only) и Telegram-уведомлении с кликабельным номером; отличим от код-фейл-отката; без дубля с ORCH-111." + module: tests/test_orch110_observability.py + expected: PASS