work_item: ORCH-115
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-16
model_used: claude-opus-4-8
title: "Детерминированный staging-раннер вместо LLM-деплойера (deploy-staging)"
framework: pytest
scope: >
  Покрывает Phase 1: перехват deployer-джоба на deploy-staging до _spawn, маппинг
  exit-кода в staging_status:, запись/merge 15-staging-log.md, инициацию существующего
  гейта check_staging_status, kill-switch/скоуп, never-raise/fail-safe, изоляцию
  процесса/таймаут, наблюдаемость, и анти-дрейф инвариант (STAGE_TRANSITIONS/QG_CHECKS/
  схема БД не тронуты). Вне покрытия: Phase 2 (project deploy contract для не-self репо),
  прод-ребро deploy (ORCH-036), живой Claude CLI и живой staging-стенд (мокируются).
notes: >
  Тесты не требуют живого Claude CLI, docker или сети: subprocess/docker-exec и
  advance_stage мокируются; пьюр-маппинг тестируется напрямую. Полный регресс tests/
  должен оставаться зелёным. Анти-дрейф (TC-09) защищает критический инвариант NFR-1.

tests:
  - id: TC-01
    type: unit
    description: "applies(repo): enabled=False -> False (откат к LLM); пустой CSV -> True только для orchestrator; непустой CSV -> membership; not-self репо -> False; ошибка -> False (never-raise, fail-safe)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-02
    type: unit
    description: "Маппинг exit-кода: 0 -> SUCCESS; 1/2/любой ненулевой -> FAILED; None/нечисло/ошибка запуска -> FAILED (fail-closed). Согласован с self_deploy.map_exit_code_to_status."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-03
    type: unit
    description: "Рендер 15-staging-log.md: frontmatter содержит staging_status: SUCCESS|FAILED (UPPERCASE) + 52c-схему (work_item/stage=deploy-staging/author_agent/status/created_at/model_used); INFRA-WAIVED строка из stdout копируется в тело."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-04
    type: integration
    description: "Сгенерированный раннером 15-staging-log.md читается НЕИЗМЕНЁННЫМ _parse_staging_status -> корректный (bool, reason) для SUCCESS и FAILED (контракт артефакта/гейта неизменен)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-05
    type: integration
    description: "launch_job перехватывает deployer-джоб на стадии deploy-staging для in-scope репо ДО _spawn (как D1/D2): _spawn НЕ вызывается, agent_runs не создаётся, возвращается None, jobs-строка ведётся mark_job. _spawn мокирован."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-06
    type: integration
    description: "Дискриминатор стадии: deployer-джоб на стадии deploy (не deploy-staging) НЕ перехватывается раннером (для self-hosting прод-ребро идёт через Phase A; для не-self остаётся прежний путь)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-07
    type: integration
    description: "После SUCCESS-вердикта раннер инициирует advance_stage(finished_agent='deployer') ровно как завершившийся LLM-deployer (advance_stage мокирован/наблюдается); после FAILED — тот же путь, что у FAILED LLM (откат deploy-staging -> development)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-08
    type: integration
    description: "Kill-switch: staging_runner_enabled=False -> на deploy-staging для orchestrator вызывается _spawn (прежний LLM-путь байт-в-байт), раннер не активируется."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-09
    type: unit
    description: "Анти-дрейф NFR-1: STAGE_TRANSITIONS (src/stages.py) и реестр/имена QG_CHECKS + ключ staging_status: неизменны; в схеме БД нет новой таблицы/колонки от ORCH-115. Структурная проверка."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-10
    type: integration
    description: "never-raise/fail-safe: docker exec бросает/таймаутит/возвращает ненулевой -> раннер не падает, исход FAILED (fail-closed) или штатный requeue, никогда тихий advance/ложный green; воркер/очередь не клинятся."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-11
    type: unit
    description: "Таймаут: staging_runner_timeout_s применяется к subprocess; малформ/непозитив -> дефолт + WARNING (never-break); завершение чистое (tree-kill согласован с proc_group ORCH-110)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-12
    type: unit
    description: "Self-hosting safety: в командной строке раннера нет запрещённых литералов (рестарт 8500 / docker compose up orchestrator / --build / force-push main / правки .env)."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-13
    type: integration
    description: "Наблюдаемость: GET /queue содержит блок staging_runner (enabled/repos/счётчики success/failed/tool_error/runs); на прогон пишется один структурный лог-вердикт, различающий код-фейл и tool-error."
    module: tests/test_orch115_staging_runner.py
    expected: PASS

  - id: TC-14
    type: integration
    description: "Анти-дрейф LLM-карты: llm-call-sites.md (A6)/roadmap/policy обновлены под реализацию; tests/test_llm_call_site_inventory.py и tests/test_llm_determinization_docs.py остаются зелёными после правок."
    module: tests/test_llm_call_site_inventory.py
    expected: PASS
