From 44adcba389df2f224bcf8b5f20377876ef1dcc6c Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 15 Jun 2026 01:05:57 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=674 --- docs/work-items/ORCH-111/01-brd.md | 145 ++++++++++++++++++ docs/work-items/ORCH-111/02-trz.md | 122 +++++++++++++++ .../ORCH-111/03-acceptance-criteria.md | 129 ++++++++++++++++ docs/work-items/ORCH-111/04-test-plan.yaml | 102 ++++++++++++ 4 files changed, 498 insertions(+) create mode 100644 docs/work-items/ORCH-111/01-brd.md create mode 100644 docs/work-items/ORCH-111/02-trz.md create mode 100644 docs/work-items/ORCH-111/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-111/04-test-plan.yaml diff --git a/docs/work-items/ORCH-111/01-brd.md b/docs/work-items/ORCH-111/01-brd.md new file mode 100644 index 0000000..1d3a58f --- /dev/null +++ b/docs/work-items/ORCH-111/01-brd.md @@ -0,0 +1,145 @@ +--- +work_item: ORCH-111 +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-111 — watchdog должен алертить на долго живущие pytest/дочерние процессы, блокирующие конвейер + +Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в полный цикл** + +> **Эскалация (`escalate: full-cycle`).** Задача пришла как баг (`BUG:` в заголовке), но **не** +> является дешёвым багфиксом: закрытие пробела требует **архитектурного решения** — у sidecar-watchdog +> сейчас **нет видимости процессов хоста вообще** (`network_mode: host`, но без `pid: host` и без +> bind-mount `/proc`). Выбор механизма наблюдения (расширение привилегий sidecar vs обогащение +> контракта `/metrics` орком) — это развилка с последствиями для безопасности и стабильного контракта +> `/metrics` (schema_version, ORCH-099). Поэтому пакет — **полный** (а не lite-bug), и задача +> помечена `escalate: full-cycle`: нужен прогон стадии `architecture` + ADR (механизм видимости, +> эвристика детекции, привилегии/безопасность). Оператор снимает багфикс-трек эндпоинтом +> `POST /bug-fast-track/escalate?work_item=ORCH-111` (ADR-001 D5, ORCH-019). + +## 1. Бизнес-контекст и проблема + +### 1.1 Симптом (установленный факт) +На хосте прода были обнаружены старые зависшие процессы `python3 -m pytest tests/test_install_lite_script.py`, +которые жили **более 2 суток**, грузили CPU и мешали локальному merge-gate re-test. Из-за конкуренции +за CPU задача **ORCH-109 несколько раз упиралась** в `re-test timeout after 600s` +(`merge_gate.retest_branch`). Сами эти процессы **не были подняты как отдельный alert** watchdog'а — +оператор узнал о них случайно. + +### 1.2 Локализация +- **Мониторинг-мозг** — sidecar-watchdog (ORCH-100, каталог `watchdog/`, сервис `orchestrator-watchdog`). + Он уже алертит на `stage_stuck` (стадия задачи застряла) и `container_down` (контейнер не в норме), + а также `agent_hung`, `orch_down`, `host_mem`, `queue_depth`, `job_failed`, `dep_down`. +- **Сигнал `agent_hung`** (`watchdog/signals.py::eval_envelope`) покрывает **только running-агент-джобы**: + он читает раздел `agents[]` из `GET /metrics`, а тот строится `src/metrics.py::_build_agents` по + `db.get_running_agents()` — то есть **только по `jobs.pid` активных джобов**. +- **Источник зависших процессов** — субпроцессы pytest, которые орк запускает в worktree: + `merge_gate.retest_branch` (`python -m pytest `) и `coverage_gate.measure_coverage` + (`pytest --cov=src`). При `subprocess.TimeoutExpired` Python убивает прямого ребёнка, но + **внуки/репарентированные процессы переживают**; а если сам агент-процесс убит по таймауту + (`exit_code=-9`, ORCH-109) — его дочерний pytest **репарентируется на PID 1** и продолжает жить. + +### 1.3 Причина (root cause) +Между двумя наблюдателями зияет **слепая зона**: `agent_hung` видит лишь *отслеживаемые* агент-джобы +(по `jobs.pid`), а **осиротевшие/внебюджетные тестовые субпроцессы** (внуки pytest, репарентированные +на PID 1) **не присутствуют ни в `/metrics`, ни в поле зрения sidecar** — у контейнера watchdog нет +доступа к таблице процессов хоста. Поэтому долго живущий pytest, реально блокирующий конвейер через +CPU-голодание merge-gate, **не порождает ни одного сигнала**, пока формально ни одна стадия задачи не +«застряла». + +## 2. Объём (scope) + +### В объёме +- Новый **отдельный класс алерта** watchdog'а: «долго живущий тестовый/дочерний процесс блокирует + конвейер» — поднимается, даже если стадия задачи формально не `stuck`. +- Детекция долго живущих процессов тест-класса (pytest и родственные субпроцессы гейтов), переживших + свой бюджет/осиротевших, на хосте прода. +- Актуализация конфиг-канона watchdog (`.env.watchdog.example` / блок `WATCHDOG_*` в `.env.example`) + и наблюдаемости. + +### Вне объёма +- **Любая реакция на процесс** (kill/SIGTERM/cleanup/reap/перезапуск). Задача — **только мониторинг + + сигнализация** (явное ограничение заказчика). Автоматический reap осиротевших процессов — **отдельная + задача**. +- Изменение конвейера: `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схема БД — **не + трогаются** (watchdog — наблюдатель вне процесса орка и вне Quality Gates). +- Расширение `agent_hung` на нетреканые процессы (это другой класс сигнала; дубль запрещён — см. NFR-4). +- Снятие первопричины осиротения процессов в самом орке (надёжный reap внуков pytest) — ценно, но это + **ремедиация**, она вне объёма этой задачи. + +## 3. Заинтересованные стороны +- **Заказчик/оператор прода** (Слава) — получает ранний сигнал о CPU-голодании ещё до того, как оно + завалит merge-gate re-test очередной задачи. +- **Self-hosting конвейер orchestrator** — страдает напрямую (инцидент ORCH-109). +- **Тиражные инсталляции (Lite/Bundled, ORCH-102/103)** — sidecar входит в дефолтный комплект; новый + сигнал должен укладываться в канон конфига и не ломать тираж. +- **Принимает результат** — reviewer/tester + оператор (smoke на staging-эквиваленте sidecar). + +## 4. Бизнес-требования (BR) +- **BR-1** — Watchdog поднимает **отдельный, узнаваемый** alert, когда на хосте обнаружен долго живущий + процесс тест-класса (pytest и его субпроцессы), возраст которого превышает настраиваемый порог — + **независимо** от того, застряла ли формально какая-либо стадия задачи. +- **BR-2** — Текст алерта **действенно идентифицирует** виновника: фрагмент командной строки, PID, + возраст процесса и (при наличии) доля CPU — чтобы оператор мог сразу разобраться и вручную вмешаться. +- **BR-3** — **Только мониторинг + сигнализация.** Watchdog **не убивает / не останавливает / не шлёт + сигналы** процессу и не выполняет иную ремедиацию (жёсткое ограничение заказчика, рамка C-1 + «наблюдатель строго read-only к наблюдаемому», ORCH-100). +- **BR-4** — **Без ложных срабатываний** на легитимных in-flight прогонах: тестовый процесс, + принадлежащий **активному отслеживаемому** агенту/гейту в пределах его бюджета, alert поднимать + **не должен**. +- **BR-5** — Анти-спам и recovery как у прочих сигналов: один alert на пересечение порога, throttled + re-alert по cooldown, однократный recovery при исчезновении процесса (переиспользовать + `watchdog/decision.py::decide` + `AlertState`). +- **BR-6** — Сигнал под **kill-switch** и управляется конфигом (порог возраста, cooldown, область). + Дефолт выбирается так, чтобы включение было **осознанным** и **self-hosting-безопасным** (см. NFR-3). + +## 5. Нефункциональные требования (NFR) +- **NFR-1 (надёжность)** — **never-raise** на всех новых путях (per-source / per-tick / per-send), как + и весь watchdog: сбой коллектора процессов деградирует ОДИН сигнал, а не роняет тик. +- **NFR-2 (read-only)** — строго наблюдение: **ни одного** управляющего действия над процессами/хостом + (нет `kill`/`signal`/`Popen`/записи). Соответствует C-1 (observer separated from observed). +- **NFR-3 (self-hosting безопасность)** — выкат изменения **не перезапускает** прод-контейнер + `orchestrator` (встанет конвейер всех проектов): пересобирается/рестартится **только** контейнер + `orchestrator-watchdog`. Если механизм требует расширения привилегий sidecar (напр. `pid: host`) — + это привилегия **только наблюдателя**, обоснование и риски — задача архитектора (ADR). +- **NFR-4 (без дубля)** — новый сигнал **не пересекается** с `agent_hung` (тот уже покрывает + отслеживаемые агент-джобы): новый сигнал закрывает ровно пробел «нетреканый/осиротевший процесс». +- **NFR-5 (канон тиража)** — при изменении compose / ключей `.env.watchdog` обновить в **том же PR**: + `.env.watchdog.example`, блок `WATCHDOG_*` в `.env.example`, `docs/deployment/LITE_SETUP.md` и + `docs/architecture/README.md` (норматив сопровождения ORCH-102 NFR-5; key-set-sync тест). +- **NFR-6 (стек)** — sidecar остаётся **stdlib-only** (C-3, ORCH-100): без новых сторонних зависимостей. + +## 6. Допущения и ограничения +- **Ключевое архитектурное допущение (для архитектора):** у контейнера `orchestrator-watchdog` сейчас + **нет** видимости процессов хоста (`network_mode: host`, но без `pid: host` и без mount `/proc`). + Закрытие пробела требует выбора механизма — **развилка, решаемая ADR**, не аналитиком. Кандидаты + (перечислены как материал для решения, **без навязывания**): (a) расширение привилегий sidecar — + `pid: host` либо read-only mount хостового `/proc`, затем stdlib-скан таблицы процессов; (b) + обогащение `/metrics` орком новым read-only разделом о «бесхозных» тест-субпроцессах (орк видит свой + PID-namespace), который sidecar лишь читает. У каждого — свои trade-off'ы (привилегии vs контракт + `/metrics`). +- `/metrics` — **версионированный контракт** (`schema_version`, ORCH-099): если выбран путь (b), + аддитивные изменения **не бампят** версию (sidecar обязан толерировать). +- Порог возраста для детекции **должен превышать** максимальный легитимный бюджет тест-прогона + (`merge_retest_timeout_s` ≈ 600s, `coverage_run_timeout_s`), чтобы нормальный прогон **никогда** не + алертил, а 2-суточный осиротевший pytest — гарантированно (анти-false-positive, материал для ADR). +- enduro-trails не затронут: watchdog наблюдает хост/орк self-hosting; сигнал config-gated. + +## 7. Критерии успеха +Watchdog при наличии долго живущего pytest/дочернего процесса, грузящего CPU, **поднимает отдельный +alert** в свой Telegram-канал (с PID/cmd/возрастом), **не трогая** процесс; при отсутствии такого +процесса (или выключенном флаге) — молчит; нормальный тест-прогон под активным джобом **не** триггерит +ложный alert. Детальные PASS/FAIL — `03-acceptance-criteria.md`. + +## 8. Риски +- **Ложные срабатывания** на легитимном длинном прогоне → спам в канал (митигируется порогом > + макс. бюджета + корреляцией с активным джобом). +- **Расширение привилегий sidecar** (если выбран `pid: host`/`/proc`-mount) → увеличение поверхности + безопасности наблюдателя (требует явного обоснования в ADR; дефолт-off). +- **Дубль с `agent_hung`** при небрежной реализации (NFR-4). +- Детали и владельцы рисков — `10-tech-risks.md` (заполняет архитектор). diff --git a/docs/work-items/ORCH-111/02-trz.md b/docs/work-items/ORCH-111/02-trz.md new file mode 100644 index 0000000..50e27d5 --- /dev/null +++ b/docs/work-items/ORCH-111/02-trz.md @@ -0,0 +1,122 @@ +--- +work_item: ORCH-111 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# 02 — ТЗ (TRZ): ORCH-111 — alert на долго живущие pytest/дочерние процессы в watchdog + +Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. +> Архитектурное обоснование/решения — задача архитектора (06-adr). В частности, **выбор механизма +> видимости процессов хоста** (см. §2 «развилка») — за ADR; ниже зафиксированы **требования и +> ограничения**, а не способ реализации. + +## 1. Сводка изменения +Добавить в sidecar-watchdog (`watchdog/`) **новый сигнал** класса «долго живущий блокирующий +тест/дочерний процесс» (рабочее имя ключа — `proc_blocking`; финальное имя утверждает разработчик/ADR). +Сигнал активен, когда на хосте есть процесс тест-класса (pytest и его субпроцессы), чей возраст +превысил настраиваемый порог и который **не атрибутируется активному отслеживаемому джобу**. Сигнал +проходит через **существующую** машину `decide()`/`AlertState` (анти-спам/recovery) и публикуется в +собственный Telegram-канал sidecar. Watchdog при этом **не трогает** процесс (BR-3/NFR-2). Это +изменение **внутри наблюдателя**: машина стадий орка и Quality Gates не затрагиваются. + +## 2. Задействованные модули / пути + +> **Развилка механизма (решает архитектор, ADR).** Часть путей **условна** и зависит от выбранного +> механизма видимости процессов. Ниже помечено явно. + +| Путь | Действие | +|------|----------| +| `watchdog/signals.py` | изменить — чистый builder нового сигнала `proc_blocking` (по образцу `host_signals`/`container_signals`) | +| `watchdog/config.py` | изменить — новые ключи `WATCHDOG_PROC_*` (enable/порог возраста/паттерны/cooldown); never-raise парсеры | +| `watchdog/core.py` | изменить — врезка коллектора процессов + диспетч нового сигнала в `tick()` (per-source guard) | +| `watchdog/collectors/proc.py` | **создать** — коллектор списка процессов-кандидатов (механизм — по ADR); never-raise → `[]` | +| `.env.watchdog.example` | изменить — задокументировать новые `WATCHDOG_PROC_*` ключи (канон) | +| `.env.example` (блок `WATCHDOG_*`) | изменить — key-set-sync с `.env.watchdog.example` (тест `test_lite_setup_doc`/key-sync) | +| `tests/watchdog/test_proc_blocking_signal.py` | **создать** — unit + регресс на новый сигнал | +| `tests/watchdog/test_tick_proc_blocking_integration.py` | **создать** — интеграция tick→dispatch | +| `docs/architecture/README.md`, `docs/deployment/LITE_SETUP.md` | изменить — описать сигнал/ключи (NFR-5) | +| `docker-compose.yml` (сервис `orchestrator-watchdog`) | **условно** изменить — привилегия/mount (`pid: host` или `/proc:ro`) **только если** ADR выберет watchdog-side host-скан | +| `src/metrics.py` (`_build_*`, аддитивный раздел) | **условно** изменить — **только если** ADR выберет orch-side обогащение `/metrics`; **аддитивно**, без бампа `schema_version` | + +## 3. Функциональные требования + +### FR-1 — Новый сигнал `proc_blocking` (чистый builder) +В `watchdog/signals.py` добавить чистую функцию-builder (без I/O), которая по списку записей о +процессах-кандидатах и конфигу возвращает `Signal`-объекты: +- **Ключ** — per-entity, со **стабильной идентичностью** процесса (напр. `("proc_blocking", pid)` или + хеш `cmdline`), чтобы `AlertState`/cooldown работали по каждому процессу отдельно (как + `("container_down", name)`). +- **active=True** ⇔ возраст процесса `> cfg.proc_age_s` **И** командная строка матчит класс + «тест/дочерний» (паттерн pytest и родственные, конфигурируемо) **И** процесс **не** принадлежит + активному отслеживаемому джобу (анти-false-positive, BR-4). +- **title/detail** — действенные: фрагмент cmdline, PID, возраст (сек), доля/время CPU при наличии + (BR-2). Текст на русском, в стиле существующих сигналов. +- Привязка: **BR-1, BR-2, BR-4**. + +### FR-2 — Коллектор процессов-кандидатов +Создать `watchdog/collectors/proc.py` — собирает «сырьё» (список записей `{pid, cmdline, age_s, +cpu?}`) тем механизмом, который утвердит ADR. Контракт коллектора **фиксирован независимо от +механизма**: **stdlib-only** (NFR-6), **read-only** (NFR-2), **never-raise** → при любой ошибке/ +недоступности источника возвращает `[]` (один сигнал пропущен, тик жив). Привязка: **NFR-1, NFR-2, +NFR-6**. + +### FR-3 — Конфиг + kill-switch +В `watchdog/config.py` добавить ключи (имена финализирует разработчик/ADR; предложение): +`WATCHDOG_PROC_ENABLED` (kill-switch), `WATCHDOG_PROC_AGE_MIN` (порог возраста в минутах; дефолт +**должен превышать** макс. легитимный бюджет тест-прогона — см. §7), `WATCHDOG_PROC_PATTERNS` +(CSV паттернов cmdline, дефолт включает `pytest`), при необходимости отдельный +`WATCHDOG_PROC_COOLDOWN_S`. Все парсеры never-raise с безопасными дефолтами (как существующие +`_int`/`_bool`/`_csv`). Выключенный флаг → коллектор/сигнал инертны (нулевой эффект). Привязка: +**BR-6, NFR-1**. + +### FR-4 — Инвариант «только наблюдение» +На всём новом пути запрещены `os.kill`, отправка сигналов, `subprocess.Popen`/`run`, любые мутации +процессов/ФС/БД. Watchdog **только читает и уведомляет**. Привязка: **BR-3, NFR-2**. + +### FR-5 — Диспетч через существующую машину решения +Новый сигнал диспетчеризуется в `core.tick()` через тот же путь `decision.decide(...)` + +`self._states[key]` + `self._send(...)`: ALERT на пересечении порога, REALERT по cooldown, RECOVERY +при исчезновении процесса. Никакой отдельной логики анти-спама не вводить. Привязка: **BR-5**. + +### FR-6 — Без дубля с `agent_hung` +Новый сигнал покрывает **только** процессы, **не** представленные в `/metrics agents[]` (нетреканые/ +осиротевшие). Атрибуция «процесс принадлежит активному джобу» исключает такие процессы из +`proc_blocking` (предотвращает двойной алерт и ложные срабатывания на живом агенте). Привязка: +**NFR-4, BR-4**. + +## 4. Изменения API +- **Орк HTTP API:** новых эндпоинтов **не требуется**. **Условно** (если ADR выберет orch-side путь): + **аддитивный** раздел в ответе `GET /metrics` (`src/metrics.py`) о бесхозных тест-субпроцессах — + строго read-only, **без бампа** `schema_version` (ORCH-099 NFR-6), sidecar толерирует отсутствие. +- **Watchdog:** внутренний сигнал, внешнего API не имеет. + +## 5. Изменения схемы БД +Нет. + +## 6. Требования к новым/изменённым QG checks +Нет. Watchdog — наблюдатель **вне** конвейера и вне Quality Gates: `QG_CHECKS` / `check_*` / +machine-verdict ключи / `STAGE_TRANSITIONS` — **байт-в-байт не трогаются** (как `disk_watchdog`/ +`reaper`/`reconciler`). + +## 7. Совместимость / регресс +- **Kill-switch + дефолты:** при выключенном `WATCHDOG_PROC_ENABLED` (или при дефолте, выбранном + безопасно) — **нулевая регрессия**: ни одного нового алерта, тик 1:1 как до ORCH-111. Дефолтный + порог возраста **обязан превышать** максимальный легитимный бюджет тест-прогона + (`merge_retest_timeout_s` ≈ 600s, `coverage_run_timeout_s`) — иначе нормальный прогон даст ложный + alert (анти-false-positive, BR-4). +- **never-raise / read-only:** новый код не может уронить тик и не выполняет управляющих действий + (NFR-1/NFR-2). +- **Контракт `/metrics`:** при orch-side варианте — только аддитивно, без бампа версии; при + watchdog-side варианте — `/metrics` не трогается вовсе. +- **Self-hosting (NFR-3):** выкат — пересборка/рестарт **только** `orchestrator-watchdog`; прод + `orchestrator` **не** перезапускается. Если механизм требует привилегии/mount в compose — это правка + **только** сервиса watchdog. +- **Канон тиража (NFR-5):** новые ключи синхронизировать в `.env.watchdog.example` ↔ блок `WATCHDOG_*` + в `.env.example` и описать в `LITE_SETUP.md` в том же PR (key-set-sync тест должен остаться зелёным). +- **Область:** сигнал config-gated; enduro-trails не затронут. diff --git a/docs/work-items/ORCH-111/03-acceptance-criteria.md b/docs/work-items/ORCH-111/03-acceptance-criteria.md new file mode 100644 index 0000000..2b49659 --- /dev/null +++ b/docs/work-items/ORCH-111/03-acceptance-criteria.md @@ -0,0 +1,129 @@ +--- +work_item: ORCH-111 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-111 — alert на долго живущие pytest/дочерние процессы + +Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что считается +провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам. + +--- + +## AC-1 — Алерт на долго живущий блокирующий процесс + +**Условие:** на хосте есть процесс тест-класса (pytest/субпроцесс гейта), чей возраст превысил +настроенный порог и который не атрибутирован активному отслеживаемому джобу. +- **PASS:** watchdog поднимает **отдельный** сигнал (рабочий ключ `proc_blocking`) → ALERT в свой + Telegram-канал; алерт срабатывает **даже если** ни одна стадия задачи формально не `stuck`. +- **FAIL:** такой процесс присутствует, но ни одного нового алерта не поднято (поведение до ORCH-111). + +--- + +## AC-2 — Действенный текст алерта + +**Условие:** алерт `proc_blocking` сформирован. +- **PASS:** `detail` содержит фрагмент cmdline (видно, что это pytest/тест-процесс), PID, возраст + процесса в секундах и (при наличии) долю/время CPU. +- **FAIL:** алерт без идентификации процесса (нельзя понять, какой PID/команду проверять). + +--- + +## AC-3 — Только наблюдение, без ремедиации + +**Условие:** обнаружен блокирующий процесс. +- **PASS:** на всём новом пути нет `os.kill`/отправки сигналов/`subprocess.Popen|run`/иных мутаций; + watchdog **только** алертит, процесс остаётся жив (его судьба — на операторе). +- **FAIL:** код пытается убить/остановить/просигналить процесс или иначе ремедиировать. + +--- + +## AC-4 — Без ложных срабатываний на легитимном прогоне + +**Условие:** на хосте идёт нормальный тест-прогон (merge-gate re-test / coverage) под активным +отслеживаемым джобом, в пределах своего бюджета. +- **PASS:** `proc_blocking` **не** активен для такого процесса (возраст ниже порога **или** процесс + атрибутирован активному джобу) → алерта нет. +- **FAIL:** легитимный прогон под активным джобом триггерит ложный `proc_blocking`-alert. + +--- + +## AC-5 — Без дубля с `agent_hung` + +**Условие:** процесс уже представлен в `/metrics agents[]` (отслеживаемый running-агент). +- **PASS:** для него работает только существующий `agent_hung`; `proc_blocking` его **не** дублирует + (ровно один класс алерта на один процесс). +- **FAIL:** один и тот же процесс порождает и `agent_hung`, и `proc_blocking` (двойной алерт). + +--- + +## AC-6 — Анти-спам и recovery + +**Условие:** блокирующий процесс держится несколько тиков, затем исчезает. +- **PASS:** один ALERT при пересечении порога; в пределах cooldown — `NONE` (не спамит); REALERT + по истечении cooldown; **однократный** RECOVERY при исчезновении процесса (через + `decision.decide`/`AlertState`). +- **FAIL:** алерт на каждом тике (спам) либо отсутствует recovery. + +--- + +## AC-7 — Kill-switch и нулевая регрессия + +**Условие:** `WATCHDOG_PROC_ENABLED` выключен (или дефолт-off). +- **PASS:** коллектор/сигнал инертны; набор и поведение тика **байт-в-байт** как до ORCH-111; полный + `pytest tests/` зелёный; watchdog-тесты зелёные. +- **FAIL:** при выключенном флаге появляется новый алерт/сетевой оверхед, либо падают существующие + тесты/тик. + +--- + +## AC-8 — never-raise / read-only + +**Условие:** коллектор процессов получает битый/пустой/недоступный источник. +- **PASS:** коллектор деградирует в `[]` (один сигнал пропущен), тик завершается штатно; ни одной + записи/мутации/управляющего действия над процессами/ФС/БД; sidecar остаётся stdlib-only. +- **FAIL:** исключение роняет тик, либо появляется новая сторонняя зависимость, либо мутирующая + операция. + +--- + +## AC-9 — Конвейер и контракты не тронуты + +**Условие:** изменение влито. +- **PASS:** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict ключи / схема БД — + без изменений; правка `/metrics` (если выбрана) — **аддитивна**, `schema_version` не бампнут; + выкат не требует рестарта прод-контейнера `orchestrator`. +- **FAIL:** затронут любой из перечисленных контрактов или бампнут `schema_version` без ломающего + изменения, либо выкат требует рестарта прод-орка. + +--- + +## AC-10 — Канон конфига/тиража синхронизирован + +**Условие:** добавлены ключи `WATCHDOG_PROC_*` / изменён compose. +- **PASS:** ключи присутствуют и согласованы в `.env.watchdog.example` ↔ блоке `WATCHDOG_*` + `.env.example`; описаны в `LITE_SETUP.md` и `docs/architecture/README.md` в том же PR; key-set-sync + тест зелёный. +- **FAIL:** ключи добавлены только в код, канон/доки тиража не обновлены (дрейф). + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1, FR-2, FR-5 | +| AC-2 | BR-2 / FR-1 | +| AC-3 | BR-3 / FR-4 / NFR-2 | +| AC-4 | BR-4 / FR-1, FR-6 | +| AC-5 | NFR-4 / FR-6 | +| AC-6 | BR-5 / FR-5 | +| AC-7 | BR-6 / FR-3 / §7 | +| AC-8 | NFR-1, NFR-2, NFR-6 / FR-2 | +| AC-9 | §6 / NFR-3 / §4 | +| AC-10 | NFR-5 / §7 | diff --git a/docs/work-items/ORCH-111/04-test-plan.yaml b/docs/work-items/ORCH-111/04-test-plan.yaml new file mode 100644 index 0000000..1c4ef20 --- /dev/null +++ b/docs/work-items/ORCH-111/04-test-plan.yaml @@ -0,0 +1,102 @@ +work_item: ORCH-111 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-15 +model_used: claude-opus-4-8 +title: "Watchdog alert на долго живущие pytest/дочерние процессы, блокирующие конвейер" +framework: pytest +scope: > + Покрывает новый сигнал watchdog `proc_blocking`: чистый builder, конфиг/kill-switch, + never-raise/read-only коллектор, анти-спам/recovery, отсутствие дубля с agent_hung и + диспетч в tick(). Вне покрытия: автоматический reap/kill процессов (ремедиация — вне + объёма задачи) и любые изменения конвейера/Quality Gates. +notes: > + Эталон тестируемости — чистые функции `watchdog/signals.py` + `watchdog/decision.py` + (тесты без контейнера/сокета/таймера, см. tests/watchdog/test_decision.py, + test_host_collector.py). TC-01 и TC-07 — ОБЯЗАТЕЛЬНЫЙ регресс-тест бага: красный до + фикса (сигнал/диспетч отсутствуют → нет алерта на долго живущий процесс), зелёный + после. Точная форма теста КОЛЛЕКТОРА (host-scan vs orch-/metrics enrichment) зависит + от механизма, утверждаемого архитектором (ADR); детерминированные TC ниже якорятся на + чистом сигнале + decision-поверхности и не зависят от выбора механизма. Полный регресс + `pytest tests/` обязан оставаться зелёным. + +tests: + - id: TC-01 + type: unit + description: > + РЕГРЕСС (красный→зелёный): builder сигнала по записи о процессе с возрастом > + порога и cmdline класса pytest, не принадлежащему активному джобу, возвращает + активный Signal с ключом `proc_blocking`. До фикса — алерт отсутствует. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-02 + type: unit + description: > + Анти-false-positive: процесс с возрастом НИЖЕ порога ИЛИ атрибутированный активному + отслеживаемому джобу → сигнал НЕ активен (нет алерта). Покрывает BR-4. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-03 + type: unit + description: > + Конфиг/kill-switch: `WATCHDOG_PROC_*` парсятся с безопасными дефолтами; дефолтный + порог возраста превышает merge_retest_timeout_s; выключенный `WATCHDOG_PROC_ENABLED` + делает коллектор/сигнал инертными (нулевая регрессия). Покрывает BR-6/FR-3/AC-7. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-04 + type: unit + description: > + never-raise/read-only коллектора: битый/пустой/недоступный источник → `[]` (один + сигнал пропущен), без исключения; на пути нет os.kill/signal/subprocess/мутаций. + Покрывает NFR-1/NFR-2/AC-3/AC-8. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-05 + type: unit + description: > + Анти-спам/recovery через decision.decide+AlertState: ALERT при пересечении порога, + NONE в пределах cooldown, REALERT по истечении, однократный RECOVERY при исчезновении + процесса. Покрывает BR-5/AC-6. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-06 + type: unit + description: > + Без дубля с agent_hung: процесс, присутствующий в /metrics agents[], не порождает + `proc_blocking` (исключён атрибуцией к активному джобу). Покрывает NFR-4/FR-6/AC-5. + module: tests/watchdog/test_proc_blocking_signal.py + expected: PASS + + - id: TC-07 + type: integration + description: > + РЕГРЕСС tick→dispatch: Watchdog.tick() с инъектированным коллектором, отдающим долго + живущий блокирующий процесс, диспетчеризует `proc_blocking`-алерт через fake-Notifier; + при выключенном флаге алерт не отправляется. Покрывает BR-1/FR-5/AC-1/AC-7. + module: tests/watchdog/test_tick_proc_blocking_integration.py + expected: PASS + + - id: TC-08 + type: integration + description: > + Конфиг-канон/тираж: key-set `.env.watchdog.example` ↔ блок WATCHDOG_* в `.env.example` + синхронизирован после добавления `WATCHDOG_PROC_*` (key-sync/LITE_SETUP тест зелёный). + Покрывает NFR-5/AC-10. + module: tests/watchdog/test_config_killswitch.py + expected: PASS + + - id: TC-09 + type: integration + description: > + Полный регресс: `pytest tests/` зелёный; конвейер/Quality Gates не затронуты + (STAGE_TRANSITIONS/QG_CHECKS/схема БД без изменений); при orch-side варианте + schema_version /metrics не бампнут. Покрывает AC-9. + module: tests/ + expected: PASS