work_item: ORCH-116 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-16 model_used: claude-opus-4-8 title: "Детерминированный test-раннер вместо LLM-тестера (стадия testing)" framework: pytest scope: > Покрывает Phase 1: перехват tester-джоба на стадии testing до _spawn, исполнение тест-контракта (pytest через proc_group + опц. read-only smoke), маппинг exit-кода в result: PASS|FAIL, запись/push 13-test-report.md, инициацию существующего гейта check_tests_passed, two-level outcome (tool-error DEFER, анти-ORCH-110), kill-switch/ скоуп/backward-compat для репо без тест-контракта, never-raise/fail-safe, изоляцию процесса/таймаут (tree-kill), гибрид (LLM не в control-path вердикта), наблюдаемость, и анти-дрейф инвариант (STAGE_TRANSITIONS/QG_CHECKS/check_tests_passed/_parse_tests_verdict/ схема БД не тронуты). Вне покрытия: Phase 2 (project test contract для не-self репо, off-control-path LLM-триаж), ORCH-115 staging/deploy-раннер, LLM-роли reviewer/developer, живой Claude CLI и живой прод-стенд (мокируются). notes: > Тесты не требуют живого Claude CLI или сети: subprocess/pytest-run (proc_group) и advance_stage мокируются; пьюр-маппинг и рендер frontmatter тестируются напрямую; smoke-GET мокируются. Полный регресс tests/ должен оставаться зелёным. Анти-дрейф (TC-09) защищает критический инвариант NFR-1. Эталон реализации/покрытия — tests/test_orch115_staging_runner.py (ORCH-115). tests: - id: TC-01 type: unit description: "applies(repo): enabled=False -> False (откат к LLM); пустой CSV -> True только для orchestrator; непустой CSV -> membership; репо без резолвимого тест-контракта -> False (BR-9 backward-compat); ошибка -> False (never-raise, fail-safe)." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-02 type: unit description: "Маппинг exit-кода: 0 -> PASS; 1/2/любой ненулевой -> FAIL; None/нечисло/ошибка запуска -> FAIL (fail-closed). Токены PASS/FAIL согласованы с _parse_tests_verdict; второй несогласованный маппинг не вводится." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-03 type: unit description: "Рендер 13-test-report.md: frontmatter содержит result: PASS|FAIL (UPPERCASE) + 52c-схему (work_item/stage=testing/author_agent=test-runner/status/created_at/model_used=n/a); хвост stdout pytest и smoke-итог копируются в тело." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-04 type: integration description: "Сгенерированный раннером 13-test-report.md читается НЕИЗМЕНЁННЫМ _parse_tests_verdict -> корректный (bool, reason) для PASS и FAIL (контракт артефакта/гейта check_tests_passed неизменен)." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-05 type: integration description: "launch_job перехватывает tester-джоб на стадии testing для in-scope репо ДО _spawn (как D1/D2/ORCH-115): _spawn НЕ вызывается, agent_runs не создаётся, возвращается None, jobs-строка ведётся mark_job. _spawn мокирован." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-06 type: integration description: "Дискриминатор: tester-джоб на стадии НЕ testing (защита) и любой не-tester джоб НЕ перехватываются раннером; should_intercept never-raise -> False при DB-сбое (fall-through к _spawn)." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-07 type: integration description: "После PASS-вердикта раннер инициирует advance_stage(finished_agent='tester') ровно как завершившийся LLM-tester (advance_stage мокирован/наблюдается) -> check_tests_passed -> testing->deploy-staging; после FAIL — существующий откат testing->development + developer-retry (stage_engine.py:849)." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-08 type: integration description: "Kill-switch: test_runner_enabled=False -> на testing для orchestrator вызывается _spawn (прежний LLM-путь байт-в-байт), раннер не активируется." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-09 type: unit description: "Анти-дрейф NFR-1: STAGE_TRANSITIONS (src/stages.py), реестр/имена QG_CHECKS, check_tests_passed/_parse_tests_verdict и токены result:/verdict:/status: неизменны; в схеме БД нет новой таблицы/колонки от ORCH-116. Структурная проверка." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-10 type: integration description: "Two-level outcome (анти-ORCH-110): сюита НЕ исполнилась (spawn-error/таймаут/returncode None) -> bounded DEFER (re-queue tester-джоба + restart-safe маркер), БЕЗ отката на development и БЕЗ расхода developer-retry; на исчерпании test_runner_infra_max_retries -> fail-closed FAIL + advance + INFRA-alert. Никогда тихий advance/ложный green." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-11 type: unit description: "never-raise/fail-safe: pytest-run бросает/таймаутит/worktree-ошибка -> раннер не падает, исход FAIL (fail-closed) или bounded DEFER, никогда тихий advance/ложный green; воркер/очередь не клинятся. Все публичные функции test_runner.py never-raise." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-12 type: unit description: "Изоляция/таймаут: pytest исполняется в worktree ветки задачи через proc_group.run_in_process_group (tree-kill); test_runner_timeout_s применяется; малформ/непозитив -> дефолт 900 + WARNING (never-break); reaper_max_running_s не правится." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-13 type: unit description: "Self-hosting safety: в командах раннера нет запрещённых литералов (рестарт 8500 / docker compose up orchestrator / --build / force-push main / правки .env); smoke-запросы строго read-only GET (/health,/status,/queue); лог пушится только в фичеветку." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-14 type: integration description: "Наблюдаемость + гибрид: GET /queue содержит блок test_runner (enabled/repos/target/счётчики runs/pass/fail/tool_error/deferred); на прогон пишется один структурный лог-вердикт, различающий код-фейл и tool-error; LLM не вызывается для вынесения result: в happy/fail-path." module: tests/test_orch116_test_runner.py expected: PASS - id: TC-15 type: integration description: "Анти-дрейф LLM-карты: llm-call-sites.md (A5)/roadmap (rank 2)/policy обновлены под реализацию (инвариант 'ровно один first_slice=yes' цел); tests/test_llm_call_site_inventory.py и tests/test_llm_determinization_docs.py остаются зелёными после правок." module: tests/test_llm_call_site_inventory.py expected: PASS