work_item: ORCH-065 description: > Тест-план для фикса zombie jobs (job-reaper), залипшего merge-lease (проактивный реклейм dead/stale lease) и идемпотентной финализации merge. Все новые фоновые механизмы — never-raise, restart-safe, kill-switch. Модуль reaper и точные имена функций уточнит архитектор; в module указаны кандидатные пути (tests/test_job_reaper.py / tests/test_merge_lease_reclaim.py). tests: # ---- A. Job-reaper ---- - id: TC-01 type: unit description: > Reaper переводит running-job с мёртвым исполнителем в корректный статус без рестарта процесса (pid не существует / exit_code записан, job всё ещё running). Покрывает AC-1. module: tests/test_job_reaper.py expected: PASS - id: TC-02 type: unit description: > Анти-ложноположительность: running-job с ЖИВЫМ процессом в пределах agent_timeout НЕ реапится (ни по одному тику, ни в пределах reaper_max_running_s). Покрывает AC-3. module: tests/test_job_reaper.py expected: PASS - id: TC-03 type: unit description: > Устойчивость: job помечается зомби только после reaper_dead_ticks последовательных тиков смерти pid (>=2), а не на первом тике. Покрывает FR-1.3. module: tests/test_job_reaper.py expected: PASS - id: TC-04 type: unit description: > Backstop по потолку: job, висящий running дольше reaper_max_running_s, реапится даже если liveness определить нельзя. Покрывает FR-1.1/AC-1. module: tests/test_job_reaper.py expected: PASS - id: TC-05 type: unit description: > Корректный исход: exit_code==0 -> успешное завершение без дублирования stage-advance; неуспех при attempts queued; исчерпан бюджет -> failed + Telegram. Покрывает AC-4. module: tests/test_job_reaper.py expected: PASS - id: TC-06 type: unit description: > Атомарность reap-UPDATE с guard status='running': параллельная обработка одной строки (стартовый requeue_running_jobs + reaper) не приводит к двойному reap. Покрывает AC-5. module: tests/test_job_reaper.py expected: PASS - id: TC-07 type: unit description: > Kill-switch reaper_enabled=false -> reaper не трогает ни одной строки (строго прежнее поведение). Покрывает AC-14. module: tests/test_job_reaper.py expected: PASS - id: TC-08 type: unit description: > never-raise: ошибка БД/ОС внутри одного тика reaper не пробрасывается и не останавливает поток (изоляция per-job, образец reconciler). Покрывает AC-9. module: tests/test_job_reaper.py expected: PASS - id: TC-09 type: integration description: > Очередь разблокируется: после reap зомби-строки при max_concurrency=1 следующий queued-job успешно claim'ится (claim_next_job + count_running_jobs). Покрывает AC-2. module: tests/test_queue.py expected: PASS # ---- B. Stale/dead merge-lease reclaim ---- - id: TC-10 type: unit description: > Реклейм lease с мёртвым pid (os.kill(pid,0) -> ProcessLookupError) проактивно, не дожидаясь TTL и чужого acquire. Покрывает AC-6. module: tests/test_merge_lease_reclaim.py expected: PASS - id: TC-11 type: unit description: > Реклейм по TTL (age >= merge_lock_timeout_s) сохранён, с logger.warning. Покрывает AC-7. (расширяет существующий stale-lease сценарий merge_gate.) module: tests/test_merge_lease_reclaim.py expected: PASS - id: TC-12 type: unit description: > Живой lease (pid жив, age < TTL) НЕ освобождается — защита легитимного merge. Покрывает AC-8. module: tests/test_merge_lease_reclaim.py expected: PASS - id: TC-13 type: unit description: > Условность: реклейм реален только для merge_gate_repos/self-hosting; для прочих репо no-op. Покрывает AC-9. module: tests/test_merge_lease_reclaim.py expected: PASS - id: TC-14 type: unit description: > never-raise: ошибка чтения/удаления lease-файла не валит реклейм-поток. Покрывает AC-9. module: tests/test_merge_lease_reclaim.py expected: PASS - id: TC-15 type: unit description: > Kill-switch lease_reclaim_enabled=false -> проактивный реклейм отключён, остаётся лишь прежний ленивый TTL-реклейм в acquire_merge_lease. Покрывает AC-14. module: tests/test_merge_lease_reclaim.py expected: PASS # ---- C. Идемпотентная финализация merge ---- - id: TC-16 type: unit description: > Идемпотентность финализации: повторный вызов при уже слитом PR / уже актуальном main (branch_is_behind_main == False) — no-op, без ошибки и без второго merge. Покрывает AC-11. module: tests/test_merge_gate.py expected: PASS - id: TC-17 type: integration description: > Восстановление: сценарий "rebase+re-test зелёные, merge не состоялся, процесс умер" -> job доводится до queued reaper'ом и merge докатывается штатным путём без повторного дорогого re-test, когда безопасно. Покрывает AC-10. module: tests/test_merge_gate_race.py expected: PASS # ---- D/E. Инварианты и наблюдаемость ---- - id: TC-18 type: integration description: > GET /queue содержит блок reaper (enabled, interval, last_run_ts, reaped_total, last_reaped, lease_reclaimed_total). Покрывает AC-15. module: tests/test_queue.py expected: PASS - id: TC-19 type: unit description: > Контракты неизменны: STAGE_TRANSITIONS и реестр QG_CHECKS не получили новых стадий/чеков; check_branch_mergeable сигнатурно не изменён. Покрывает AC-13. module: tests/test_config.py expected: PASS - id: TC-20 type: unit description: > Новые настройки reaper_*/lease_reclaim_* присутствуют в config с дефолтами и читаются из ORCH_* env. Покрывает §5 ТЗ / AC-14. module: tests/test_config.py expected: PASS - id: TC-21 type: unit description: > Стартовый реклейм stale/dead lease вызывается в lifespan рядом с requeue_running_jobs (smoke на регистрацию старт/стоп reaper-потока). Покрывает FR-2.1 / AC-6. module: tests/test_job_reaper.py expected: PASS regression: - command: pytest tests/ -q expected: PASS note: > Полный прогон не должен ломать существующие тесты merge_gate / queue / reconciler / deploy.