21 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 |
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=False→False(откат к 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 GEThttp://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); счётчик из persistedjobs. На исчерпанииtest_runner_infra_max_retries→ fail-closedresult: FAIL+ запись лога + инициация гейта (существующий rollback) + один INFRA-alert (явно «НЕ дефект кода», кликабельный номер). Никогда тихий advance/ложный green; никогда не клинит очередь; не жжёт developer-retry на транзиентной инфре.
- restart-safe маркер (
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+ CSVtest_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_grouptree-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.