Files
orchestrator/docs/work-items/ORCH-116/03-acceptance-criteria.md

14 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-116 analysis analyst ready-for-review 2026-06-16 claude-opus-4-8

03 — Критерии приёмки (Acceptance Criteria): ORCH-116 — детерминированный test-раннер

Work Item: ORCH-116 · Repo: orchestrator · Стадия: analysis

Формат: каждый критерий имеет PASS (что должно быть истинно для приёмки) и FAIL (что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам репозитория.


AC-1 — Детерминированный перехват на testing (нет _spawn/LLM)

Условие: При включённом флаге и in-scope репо с тест-контрактом джоб tester на стадии testing обрабатывается раннером, а не LLM-агентом.

  • PASS: launch_job (src/agents/launcher.py) перехватывает джоб до _spawn (рядом с D1/D2/ORCH-115) при agent=="tester" + test_runner.should_intercept(job) (стадия задачи testing + applies(repo)); _spawn не вызывается; не создаётся строка agent_runs; джоб ведётся mark_job(...) самим раннером (_run_test_runner_jobNone). Тест воспроизводит это без живого Claude CLI.
  • FAIL: На testing для in-scope репо при включённом флаге всё ещё вызывается _spawn / создаётся agent_runs-строка LLM-tester'а.

AC-2 — Контракт артефакта 13-test-report.md неизменен

Условие: Раннер пишет тот же артефакт с тем же machine-key, что читает гейт.

  • PASS: Создаётся docs/work-items/<work_item_id>/13-test-report.md с frontmatter result: PASS|FAIL (UPPERCASE) + обязательная 52c-схема (work_item/stage: testing/ author_agent/status/created_at/model_used). Неизменённый _parse_tests_verdict читает его и возвращает корректный вердикт.
  • FAIL: Изменено имя/регистр/токены ключа result: (или legacy verdict:/status:), отсутствует frontmatter, вердикт записан только прозой; либо парсер _parse_tests_verdict пришлось менять.

AC-3 — Корректный exit-code → verdict маппинг

Условие: Exit-код тест-контракта детерминированно маппится в вердикт.

  • PASS: 0 → PASS; любой ненулевой / None / ошибка запуска → FAIL (fail-closed). Маппинг — pure-функция, согласованная по контракту с self_deploy.map_exit_code_to_status (токены PASS/ FAIL), покрыта unit-тестом на каждый класс входа. Второй несогласованный маппинг не вводится.
  • FAIL: Ненулевой код даёт PASS; ошибка/None даёт PASS (ложный green); раннер вводит второй несогласованный маппинг или нестандартные токены.

AC-4 — Эквивалентность маршрутизации (PASS / FAIL)

Условие: После вердикта конвейер ведёт себя ровно как при завершившемся LLM-tester'е.

  • PASS: PASS → раннер инициирует advance_stage(finished_agent="tester")check_tests_passed → продвижение testing → deploy-staging. FAIL → существующий откат testing → development с инкрементом developer-retry и встраиванием extract_test_failures (src/stage_engine.py:849-892), тот же исход и cap MAX_DEVELOPER_RETRIES, что у FAIL-вердикта LLM.
  • FAIL: Задача зависает на testing (гейт не инициирован); или FAIL не откатывает / откатывает иначе; или появляется новое ребро/исход.

AC-5 — Two-level outcome: tool-error ≠ code-fail (анти-ORCH-110)

Условие: Невозможность исполнить сюиту трактуется как инфра-сбой, не как провал кода.

  • PASS: Сюита исполнилась (реальный exit-код) → вердикт → advance (FAIL → существующий rollback + developer-retry). Сюита НЕ исполнилась (spawn-error / таймаут / returncode None) → bounded DEFER (re-queue tester-джоба + restart-safe маркер test-runner infra-retry, счётчик из persisted jobs), без отката на development и без расхода developer-retry; на исчерпании test_runner_infra_max_retries → fail-closed result: FAIL + advance + INFRA-alert (явно «НЕ дефект кода»).
  • FAIL: Tool-error немедленно откатывает на development и жжёт developer-retry; либо tool-error даёт PASS/тихий advance; либо DEFER бесконечен (не клинит, но и не сходится к fail-closed).

AC-6 — Инвариант скоупа: гейты/стадии/схема БД не тронуты (анти-дрейф)

Условие: Изменена только сторона продюсера, не контракт конвейера.

  • PASS: git diff не затрагивает src/stages.py::STAGE_TRANSITIONS; имена/семантику QG_CHECKS/check_tests_passed/_parse_tests_verdict/прочих check_* в src/qg/checks.py; machine-verdict-ключи и токены (result:/verdict:/status:/staging_status:/deploy_status:/ security_status:/coverage_status:); схему БД (нет новых таблиц/колонок/миграций). Анти-дрейф-тест это подтверждает.
  • FAIL: Любой из перечисленных артефактов изменён по имени/семантике/структуре.

AC-7 — Kill-switch и скоуп (обратимость)

Условие: Флаг возвращает прежнее поведение; скоуп ограничивает раннер.

  • PASS: test_runner_enabled=False → на testing запускается прежний LLM-tester через _spawn (байт-в-байт до ORCH-116). Пустой test_runner_repos → раннер активен только для orchestrator. Покрыто тестом для обоих значений флага.
  • FAIL: При выключенном флаге раннер всё равно перехватывает; либо не-self репо из скоупа перехватывается ошибочно.

AC-8 — Backward-compatibility для репо без тест-контракта

Условие: Репо без резолвимого тест-контракта обслуживается прежним LLM-tester'ом.

  • PASS: applies(repo)False, когда тест-контракт для репо не сконфигурирован/не резолвится (вне скоупа или нет команды) → should_interceptFalse_spawn (LLM-tester). enduro-trails и любой репо без контракта — 1:1 как до ORCH-116. Покрыто тестом.
  • FAIL: Репо без тест-контракта перехватывается раннером и остаётся без продюсера 13-test-report.md / зависает.

AC-9 — never-raise / fail-safe (инструмент недоступен)

Условие: Любая ошибка раннера приводит к безопасному детерминированному исходу.

  • PASS: pytest не запустился / worktree-ошибка / I/O / таймаут → раннер не роняет воркер; исход — FAIL (fail-closed) или bounded DEFER (AC-5), никогда тихий advance/ложный green. Все публичные функции test_runner.py — never-raise; applies()/should_intercept() при ошибке → False (fall-through к _spawn). Очередь всех проектов не клинится.
  • FAIL: Ошибка раннера роняет воркер/клинит очередь; либо ошибка/таймаут даёт PASS.

AC-10 — Self-hosting safety

Условие: Раннер на testing не выполняет опасных для прода действий.

  • PASS: Раннер не рестартит контейнер 8500, не выполняет docker compose up -d orchestrator/ --build, не пушит force в main, не правит .env/.env.staging/docker-compose.yml. Smoke — строго read-only GET (/health//status//queue). Лог пушится только в фичеветку (merge в main — штатным merge-gate-путём). Подтверждается ревью кода раннера + тестом отсутствия запрещённых литералов в его командах.
  • FAIL: Раннер содержит путь, рестартящий 8500 / force-push в main / правящий инфру / мутирующий smoke-запрос.

AC-11 — Изоляция процесса и таймаут (proc_group / tree-kill)

Условие: pytest-subprocess ограничен по времени и не оставляет сирот.

  • PASS: Раннер запускает pytest в worktree ветки через proc_group.run_in_process_group (отдельная группа процессов, tree-kill при таймауте, grace = agent_kill_grace_seconds) с таймаутом test_runner_timeout_s (согласован со сквозным бюджетом ORCH-065/109/110, без правки reaper_max_running_s); малформ/непозитив таймаут → дефолт + WARNING; осиротевших pytest-процессов не остаётся.
  • FAIL: Нет таймаута / зависший subprocess клинит воркер; pytest бежит в общем /repos/orchestrator (checkout-гонка); остаются сироты процессов; правится reaper_max_running_s.

AC-12 — Гибрид: LLM не в потоке управления вердикта

Условие: Детерминированный раннер — единственный исполнитель result:.

  • PASS: В Phase 1 на стадии testing (in-scope) вердикт result: производит только детерминированный код; LLM не вызывается в happy-path и в fail-path для вынесения/переопределения result:. Если добавлен off-control-path триаж — он не пишет/не меняет result: и не добавляет ребро в STAGE_TRANSITIONS.
  • FAIL: LLM вызывается для вынесения/переопределения машинного вердикта гейта; либо триаж-роль гейтит продвижение.

AC-13 — Наблюдаемость

Условие: Исход раннера виден и различим.

  • PASS: GET /queue содержит read-only блок test_runner (enabled/repos/target/timeout_s/ счётчики runs/pass/fail/tool_error/deferred); на каждый прогон — один структурный лог-вердикт (work_item/repo/exit_code/result/duration_s/outcome), различающий код-фейл и tool-error.
  • FAIL: Нет блока в /queue; исход раннера не логируется/не различим.

AC-14 — Норматив сопровождения LLM-карты/политики/витрины

Условие: Документация обновлена в том же PR (правило агентов №2 + норматив ORCH-118).

  • PASS: docs/architecture/llm-call-sites.md (строка A5) / llm-determinization-roadmap.md (rank 2) / llm-usage-policy.md отражают реализацию детерминированного tester (инвариант «ровно один first_slice = yes» НЕ нарушен); анти-дрейф-тесты (tests/test_llm_call_site_inventory.py, tests/test_llm_determinization_docs.py) зелёные; .openclaw/agents/tester.md, docs/architecture/README.md, CLAUDE.md, CHANGELOG.md, docs/overview/ обновлены.
  • FAIL: Карта/политика/roadmap/витрина не обновлены; анти-дрейф-тесты красные (reviewer: ≥P1).

AC-15 — Полный регресс зелёный

Условие: Существующий конвейер не сломан.

  • PASS: pytest tests/ -q зелёный; новый tests/test_orch116_test_runner.py зелёный; зелёные анти-дрейф LLM-тесты.
  • FAIL: Любой ранее зелёный тест становится красным; новые тесты падают.

Сводная матрица AC ↔ FR/BR

AC Покрывает
AC-1 BR-1 / FR-1
AC-2 BR-2 / FR-4
AC-3 BR-4 / FR-2 / FR-3
AC-4 BR-3 / FR-5
AC-5 NFR-2 / FR-6
AC-6 NFR-1 / FR-7
AC-7 BR-5 / FR-7
AC-8 BR-9 / FR-1 / FR-7
AC-9 NFR-2 / FR-1 / FR-6
AC-10 BR-7 / FR-2 / FR-4
AC-11 NFR-3 / NFR-4 / FR-2
AC-12 BR-8 / NFR-7 / FR-9
AC-13 BR-6 / FR-8
AC-14 NFR-6
AC-15 NFR-5 / NFR-1