Files
orchestrator/docs/work-items/ORCH-116/02-trz.md
claude-bot 50c48c2c03
All checks were successful
CI / test (push) Successful in 1m8s
analyst(ET): auto-commit from analyst run_id=738
2026-06-16 02:29:50 +03:00

21 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

02 — ТЗ (TRZ): ORCH-116 — детерминированный test-раннер вместо LLM-тестера

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

ТЗ описывает конкретные изменения к реализации, выведенные из BRD и фактического кода. Архитектурное обоснование (точный механизм перехвата, размещение раннера, способ инициации advance_stage, форма тест-контракта, лестница таймаутов, two-level outcome) — задача архитектора (06-adr/). Здесь — требования и привязка к реальным модулям src/.

1. Сводка изменения

Заменить LLM-агента tester на стадии testing (для self-hosting orchestrator) детерминированным test-раннером, перехватываемым в launch_job до _spawn (прецедент deploy-finalizer/post-deploy-monitor/staging_runner, src/agents/launcher.py:397/402/405). Раннер исполняет «тест-контракт» (регресс pytest tests/ через proc_group.run_in_process_group

  • опциональные read-only smoke-проверки), маппит exit-код в result: (0→PASS, иначе FAIL), пишет 13-test-report.md, best-effort пушит лог в фичеветку, затем инициирует существующую оценку exit-гейта check_tests_passed ровно как завершившийся LLM-tester. Контракт артефакта, гейт, STAGE_TRANSITIONS, схема БД — неизменны. Под kill-switch + скоуп-CSV + тест-контракт; never-raise; fail-closed; two-level outcome (анти-ORCH-110). Эталон реализации — src/staging_runner.py (ORCH-115).

2. Задействованные модули / пути

Путь Действие Назначение
src/test_runner.py (новый leaf) создать Детерминированный раннер: applies(repo) (kill-switch + скоуп + наличие тест-контракта), should_intercept(job) (роль tester + стадия testing), исполнение тест-контракта (pytest через proc_group, опц. smoke), маппинг exit-кода → result:, запись 13-test-report.md, best-effort push в фичеветку, инициация гейта через advance_stage(finished_agent="tester"), two-level outcome (tool-error DEFER), снапшот для /queue. Leaf-чистота по образцу staging_runner.py/self_deploy.py: на импорте только config/logging/proc_group; db/git_worktree/self_deploy/qg.checks/stage_engine/notifications — лениво внутри функций; never-raise.
src/agents/launcher.py изменить В launch_job добавить перехват до _spawn (рядом с D1/D2/ORCH-115): if job.get("agent")=="tester": from .. import test_runner; if test_runner.should_intercept(job): return self._run_test_runner_job(job). Новый метод _run_test_runner_job(job) — зеркало _run_staging_runner_job: синхронно ведёт jobs-строку через `mark_job(done
src/config.py изменить Добавить ключи (зеркало staging_runner_*/merge_retest_*): test_runner_enabled: bool = True (env ORCH_TEST_RUNNER_ENABLED), test_runner_repos: str = "" (env ORCH_TEST_RUNNER_REPOS; пусто → self-hosting only), test_runner_target: str = "tests/" (pytest-таргет тест-контракта, конвенция merge_retest_target), test_runner_timeout_s: int = 900 (см. FR-2/NFR-4), test_runner_smoke_enabled: bool = True (опц. read-only smoke), test_runner_infra_max_retries: int = 2, test_runner_infra_retry_delay_s: int = 30. Дефолты = боевое; пустой .env ⇒ поведение для in-scope.
src/main.py (GET /queue) изменить Read-only блок test_runner (флаг/скоуп/таргет/счётчики исходов) — наблюдаемость BR-6 (зеркало блока staging_runner).
.openclaw/agents/tester.md изменить (docs) Отметить, что на testing для in-scope репо с тест-контрактом стадию ведёт детерминированный код (зеркало формулировки deployer.md про staging-runner ORCH-115); LLM-ветвь testing остаётся fallback'ом под выключенным флагом / для репо без тест-контракта. Канон промпта 52d (5 секций, ключ result:) — байт-в-байт.
docs/architecture/llm-call-sites.md, llm-determinization-roadmap.md, llm-usage-policy.md изменить (docs) Норматив сопровождения ORCH-118 (NFR-6): отразить реализацию A5 (tester) — обновить инвентарь/политику/roadmap (rank 2 → реализовано, инвариант «ровно один first_slice = yes» НЕ нарушать) в том же PR; синхронно поправить tests/test_llm_call_site_inventory.py / tests/test_llm_determinization_docs.py (машинные блоки).
docs/architecture/README.md, CLAUDE.md, CHANGELOG.md, docs/overview/ изменить (docs) Компонент-карта/паспорт/чейнджлог/витрина — правило для агентов №2 + витрина ORCH-011.
tests/test_orch116_test_runner.py (новый) создать Покрытие (см. 04-test-plan.yaml).

Не трогать (NFR-1): src/stages.py::STAGE_TRANSITIONS; имена/семантику QG_CHECKS/ check_tests_passed/_parse_tests_verdict/прочих check_* в src/qg/checks.py; machine-verdict-ключи (result:/verdict:/status:/…); src/staging_runner.py (ORCH-115); LLM-роли reviewer/developer (.openclaw/agents/reviewer.md/developer.md); src/transition_lease.py (ORCH-114); src/checkout_hygiene.py (ORCH-112); src/proc_group.py (переиспользуем как есть); схему БД.

3. Функциональные требования

FR-1 — Детерминированный перехват на testing (без _spawn)

В launch_job (src/agents/launcher.py) до вызова _spawn, по образцу D1/D2/ORCH-115: если job.agent == "tester" и test_runner.should_intercept(job) истинно → не вызывать _spawn, а исполнить раннер синхронно (_run_test_runner_job). Контракт: возвращает None (нет agent_runs), сам ведёт jobs-строку (mark_job(done|failed)) как _run_staging_runner_job.

  • should_intercept(job): job.agent == "tester" И applies(job.repo) И стадия задачи (tasks.stage по job.task_id) == testing. Роль tester исполняет только стадию testing (единственный agent для входа в testing, STAGE_TRANSITIONS["review"]["agent"]), поэтому коллизии стадий нет; гард по стадии — defense-in-depth (R-1). never-raise → False (DB-сбой → fall-through к _spawn, fail-safe к LLM-пути).
  • applies(repo): test_runner_enabled=FalseFalse (откат к LLM-пути); непустой test_runner_repos → membership; пустой CSV → is_self_hosting_repo(repo); и тест-контракт для репо резолвится (BR-9: иначе False → LLM-tester). Никакой сети, проверяется первым (нулевой оверхед при выключенном флаге). never-raise → False (fail-safe к LLM-пути).

FR-2 — Исполнение тест-контракта (pytest + опц. smoke) через proc_group

Раннер исполняет регресс-команду тест-контракта — python -m pytest <test_runner_target> (дефолт tests/) — в worktree ветки задачи (git_worktree.get_worktree_path(repo, branch), НЕ в общем /repos/orchestrator: анти-checkout-гонка, как требовал промпт tester шаг 2) через proc_group.run_in_process_group (ORCH-110: отдельная группа процессов, tree-kill SIGTERM→grace→SIGKILL, grace = agent_kill_grace_seconds; subprocess_tree_kill_enabled). Таймаут — test_runner_timeout_s (дефолт 900; малформ/непозитив → дефолт + WARNING, never-break — зеркало merge_gate._resolve_retest_timeout/staging_runner._resolve_timeout). Захватывает exit-код и stdout (для тела отчёта/observability).

  • Опциональный smoke (test_runner_smoke_enabled, зеркало шага 3 промпта tester): read-only GET http://localhost:<port>/health, /status, /queue + проверка наличия блока serial_gate в /queue. Любой провал smoke → итоговый FAIL (детерминированно). Smoke — строго read-only (BR-7/AC-8): никаких мутирующих запросов к 8500.

FR-3 — Маппинг exit-кода → result:

0 → "PASS", любой ненулевой / отсутствие кода / ошибка запуска → "FAIL" (fail-closed, никогда ложный green). Pure-функция, согласованная по контракту с self_deploy.map_exit_code_to_status, но в токенах PASS/FAIL (result: использует их, а не SUCCESS/FAILED; _TESTS_POSITIVE_TOKENS/ _TESTS_NEGATIVE_TOKENS, src/qg/checks.py:222-223). Если архитектор предпочтёт единый маппер с параметризованными токенами — допустимо, но второй несогласованный маппинг не плодить (BR-4).

FR-4 — Запись и push 13-test-report.md

Раннер пишет docs/work-items/<work_item_id>/13-test-report.md в worktree фичеветки с frontmatter: result: PASS|FAIL (UPPERCASE) + обязательная 52c-схема (work_item/stage: testing/author_agent/ status/created_at/model_used) + информативное тело (таблица результата pytest / хвост stdout / smoke-итог) — зеркало staging_runner.build_staging_log/write_staging_log. author_agent: test-runner, model_used: n/a честно отражают детерминированный продюсер; ключ result: и его UPPERCASE-значение не меняются. Best-effort git add/commit/push лога в фичеветку (та же git-identity ORCH-101, актор test-runner; без отдельного PR-merge в main — гейт читает worktree → origin/main fallback, check_tests_passed_repo_path). Самостоятельный merge лога в main НЕ делать (усиливает BR-7/AC-8).

FR-5 — Инициация существующего гейта после вердикта

После записи (и best-effort push) раннер инициирует ту же оценку exit-гейта, что триггерил завершившийся LLM-tester: stage_engine.advance_stage(task_id, current_stage="testing", repo, work_item_id, branch, finished_agent="tester"). Это запускает check_tests_passed (_parse_tests_verdict читает result: из 13-test-report.md) — на PASS продвигает testing → deploy-staging; на FAIL запускает существующий откат testing → development + developer-retry (cap MAX_DEVELOPER_RETRIES, встраивание extract_test_failures, src/stage_engine.py:849-892). Никакой новой ветви маршрутизации. Lease ORCH-114 берётся внутри advance_stage как сейчас — раннер его не трогает (граница задачи О1). never-raise.

FR-6 — Two-level outcome (tool-error DEFER, анти-ORCH-110)

По образцу staging_runner.run_staging_gate:

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

FR-7 — Kill-switch, скоуп, тест-контракт (обратимость + backward-compat)

test_runner_enabled=False → перехват не срабатывает → на testing запускается прежний LLM-tester (_spawn) байт-в-байт как до ORCH-116. test_runner_repos ограничивает скоуп (пусто → только orchestrator). Репо без резолвимого тест-контракта → applies==False → LLM-tester (BR-9). Переключение флага туда-сюда не оставляет несовместимого состояния (артефакт/гейт неизменны).

FR-8 — Наблюдаемость

  • Read-only блок test_runner в GET /queue: enabled, repos, target, timeout_s, infra_max_retries, счётчики runs/pass/fail/tool_error/deferred (in-process, паттерн staging_runner._STAGING_RUNNER_COUNTERS).
  • Один структурный лог-вердикт на прогон (work_item/repo/exit_code/result/duration_s/outcome), различающий «код упал» (FAIL от сюиты) и «инструмент недоступен» (tool-error).

FR-9 — Гибрид: LLM строго off-control-path (BR-8 / NFR-7)

В Phase 1 LLM на стадии testing отсутствует в потоке управления вердикта (детерминированный раннер — единственный продюсер result:). Архитектура раннера не должна архитектурно исключать будущий опциональный off-control-path LLM-триаж/диагностику после детерминированного FAIL (отдельная роль/джоб, не выносящая и не переопределяющая result:). В этом срезе он не реализуется и не добавляется в STAGE_TRANSITIONS.

4. Изменения API

  • GET /queue — добавить read-only ключ test_runner (наблюдаемость). Существующие поля ответа не меняются.
  • Опционально (на усмотрение архитектора, по образцу POST /coverage/baseline): обязательного нового мутирующего эндпоинта нет. Откат — через env-флаг.
  • Новых вебхуков нет.

5. Изменения схемы БД

Нет. Раннер использует существующие таблицы (tasks для стадии/branch/work_item_id, jobs для статуса джоба и restart-safe счётчика infra-retry по маркеру в task_content) и worktree-механику. Никаких новых таблиц/колонок/миграций (NFR-1). Счётчики /queue — in-process (паттерн _STAGING_RUNNER_COUNTERS/_MERGE_GATE_COUNTERS), не БД.

6. Требования к новым/изменённым QG checks

Нет новых QG и нет изменений существующих. check_tests_passed / _parse_tests_verdict / ключ result: (+ legacy verdict:/status:) (src/qg/checks.py:182/226) и состав QG_CHECKSбайт-в-байт неизменны. ORCH-116 меняет только продюсера 13-test-report.md (детерминированный код вместо LLM); гейт, читающий артефакт, остаётся прежним. Это критический инвариант (NFR-1) — reviewer ловит любое изменение имени/семантики гейта/парсера/токенов как finding ≥P1.

7. Совместимость / регресс

  • Обратная совместимость: test_runner_enabled=False → прежний LLM-tester-путь байт-в-байт; репо без тест-контракта / вне скоупа → LLM-tester (BR-9). enduro-trails не затронут (NFR-5).
  • Kill-switch / область раската: флаг test_runner_enabled + CSV test_runner_repos (пусто → self-hosting only). Откат = ORCH_TEST_RUNNER_ENABLED=false.
  • Обратимость: полностью обратимо флагом; артефакт и гейт неизменны, переключение туда-сюда не оставляет несовместимого состояния.
  • never-raise / fail-safe (NFR-2): ошибка раннера → FAIL (fail-closed) или bounded requeue, не «тихий advance»; tool-error не жжёт developer-retry (анти-ORCH-110). Self-hosting safety (BR-7): никаких рестартов 8500 / force-push в main / правок инфры; smoke строго read-only.
  • Изоляция (NFR-3): pytest через proc_group tree-kill (ORCH-110) — без сирот; таймаут согласован со сквозным бюджетом ORCH-065/109/110 без правки reaper_max_running_s (NFR-4).
  • Граница (О1): код ORCH-115 (staging_runner), ORCH-112 (checkout hygiene), ORCH-114 (transition lease) и LLM-роли reviewer/developer не модифицируются.
  • Норматив сопровождения (NFR-6): в том же PR обновить docs/architecture/llm-call-sites.md (A5) / llm-determinization-roadmap.md (rank 2) / llm-usage-policy.md + анти-дрейф-тесты (tests/test_llm_call_site_inventory.py, tests/test_llm_determinization_docs.py); CLAUDE.md / docs/architecture/README.md / CHANGELOG.md / docs/overview/.

8. Phase 2 (forward-looking, вне приёмки ORCH-116)

Зафиксировано для преемственности — не реализуется в этой задаче, заводится отдельным follow-up:

  • Project test contract для не-self репо (enduro-trails): декларативный per-repo контракт test / smoke (команды + ожидаемые коды/эндпоинты), исполняемый тем же детерминированным раннер-паттерном (run → map exit code → result: → artifact → gate).
  • Опциональный off-control-path LLM-триаж после детерминированного FAIL: human-readable диагностика причин падений, анализ пробелов покрытия AC, сопоставление TC ↔ критерии приёмки — как обогащение отчёта/комментария, не как продюсер вердикта result: (NFR-7).
  • Зависимость: устойчивый Phase 1 (этот work item) как доказанный паттерн перехвата + маппинга + two-level outcome.