14 KiB
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_job→None). Тест воспроизводит это без живого 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с frontmatterresult: PASS|FAIL(UPPERCASE) + обязательная 52c-схема (work_item/stage: testing/author_agent/status/created_at/model_used). Неизменённый_parse_tests_verdictчитает его и возвращает корректный вердикт. - FAIL: Изменено имя/регистр/токены ключа
result:(или legacyverdict:/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), тот же исход и capMAX_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-queuetester-джоба + restart-safe маркерtest-runner infra-retry, счётчик из persistedjobs), без отката наdevelopmentи без расхода developer-retry; на исчерпанииtest_runner_infra_max_retries→ fail-closedresult: 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_intercept→False→_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 |