work_item: ORCH-027 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 title: "Code coverage gate — защита от деградации покрытия тестами" framework: pytest scope: > Покрываются: чистая логика вердикта покрытия (режимы absolute/baseline/both, границы, epsilon), ratchet-обновление базовой линии, условность (kill-switch + per-repo область), fail-open/fail-closed при ошибке инструмента, never-raise, наблюдаемость (GET /queue, Telegram при FAIL), интеграция гейта в advance_stage / точку конвейера. Вне покрытия: фактические измерители не-Python стеков (jest/jacoco), мутационное тестирование. notes: > Тесты не должны исполнять реальный прод-деплой и не трогают prod-контейнер/main. Измерение покрытия в тестах мокается/стабится (фиктивная метрика), реальный pytest-прогон под coverage проверяется отдельным интеграционным тестом на минимальном фикстур-репо/worktree. Полный регресс tests/ должен оставаться зелёным (нулевая регрессия для enduro-trails). tests: - id: TC-01 type: unit description: "compute_coverage_verdict, policy=absolute: measured>=floor → PASS; measured=baseline → PASS; ниже baseline-epsilon → FAIL (no-regression / ratchet)" module: tests/test_coverage_gate.py expected: PASS - id: TC-03 type: unit description: "compute_coverage_verdict, policy=both: PASS только при выполнении обоих условий; нарушение любого → FAIL" module: tests/test_coverage_gate.py expected: PASS - id: TC-04 type: unit description: "epsilon-допуск: дрожание покрытия в пределах epsilon у границы не заворачивает задачу (анти-флап, NFR-4)" module: tests/test_coverage_gate.py expected: PASS - id: TC-05 type: unit description: "Ratchet базовой линии: при слиянии baseline растёт до смёрженного покрытия только если >= текущей; меньшее значение не понижает baseline" module: tests/test_coverage_gate.py expected: PASS - id: TC-06 type: unit description: "Bootstrap базовой линии: первичная инициализация фактическим покрытием main при отсутствии сохранённого значения" module: tests/test_coverage_gate.py expected: PASS - id: TC-07 type: unit description: "Условность applies(repo): пустой coverage_gate_repos → только self-hosting (is_self_hosting_repo); repo вне области → no-op (True, 'N/A'), дорогой прогон не запускается" module: tests/test_coverage_gate.py expected: PASS - id: TC-08 type: unit description: "Kill-switch coverage_gate_enabled=False → гейт инертен, advance_stage ведёт себя 1:1 как до ORCH-027" module: tests/test_coverage_gate.py expected: PASS - id: TC-09 type: unit description: "Fail-open по умолчанию: ошибка/недоступность coverage-инструмента и coverage_tool_fail_closed=False → PASS + WARNING-лог; флаг True → FAIL (fail-closed)" module: tests/test_coverage_gate.py expected: PASS - id: TC-10 type: unit description: "never-raise: внутреннее исключение (битый вывод coverage, отсутствие worktree) перехватывается, не всплывает в advance_stage" module: tests/test_coverage_gate.py expected: PASS - id: TC-11 type: unit description: "Запись/чтение отчёта: write_coverage_report пишет coverage_status: PASS|FAIL во frontmatter; parse читает обратно из того же файла через src/frontmatter.py (single source of truth)" module: tests/test_coverage_gate.py expected: PASS - id: TC-12 type: unit description: "Self-hosting безопасность: гейт не вызывает деплой-хук, не перезапускает прод-контейнер, не пушит/форс-пушит в main/master" module: tests/test_coverage_gate.py expected: PASS - id: TC-13 type: integration description: "Гейт в конвейере: при measured ниже политики advance_stage не продвигает к деплою и инициирует откат на development (инкремент developer-retry); при PASS — продвигает штатно" module: tests/test_coverage_gate.py expected: PASS - id: TC-14 type: integration description: "Реальное измерение: pytest под coverage в ensure_worktree на минимальном фикстур-репо возвращает корректную метрику line coverage и тайм-аутится по coverage_run_timeout_s" module: tests/test_coverage_gate.py expected: PASS - id: TC-15 type: integration description: "Наблюдаемость: FAIL даёт Telegram-алерт с кликабельным номером (измеренное/порог/дельта); GET /queue несёт read-only блок coverage; совместимость — STAGE_TRANSITIONS/QG_CHECKS/существующие вердикт-ключи не изменены" module: tests/test_coverage_gate.py expected: PASS