Files
orchestrator/docs/architecture/adr/adr-0050-deterministic-test-runner.md
claude-bot 74fccf3a09
All checks were successful
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m12s
fix(testing): reconcile ORCH-116 with merged ORCH-123 (ADR renumber, CHANGELOG, env parity)
Recovery from the merge-gate rebase-conflict bounce. The feature branch was
rebased onto origin/main (which had merged ORCH-123). The single conflicting
hunk — docs/architecture/README.md — was resolved during the rebase: kept
ORCH-123's host-side staging-runner line AND the ORCH-116 test-runner bullet.

This follow-up commit reconciles the remainder:

- Renumber the global sweeping ADR adr-0049 -> adr-0050. ORCH-123 took adr-0049
  (adr-0049-host-side-docker-execution-boundary.md) on main while ORCH-116 was
  in flight, so ORCH-116 yields to the merged task and moves to the next free
  number. Mechanical cross-reference reconciliation only (git mv + title + every
  test-runner reference across README/internals/CLAUDE/CHANGELOG/config.py +
  06-adr/ADR-001 + 12-review). Main's adr-0049 host-side references are left
  byte-for-byte untouched. No design/verdict content was altered.
- Restore the ORCH-116 CHANGELOG entry that the CHANGELOG auto-merge silently
  dropped (both ORCH-123 and ORCH-116 inserted at the same [Unreleased] anchor;
  git kept only ORCH-123).
- Add the missing ORCH_TEST_RUNNER_* keys to .env.example (parity with the
  ORCH_STAGING_RUNNER_* block; ORCH-101 canon of start keys).

Refs: ORCH-116

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:56:47 +03:00

9.9 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 architecture architect proposed 2026-06-16 claude-opus-4-8

adr-0050: Детерминированный test-раннер — второй реализованный срез determinization-roadmap (tester-гибрид)

Сквозной (cross-cutting) ADR. Агрегирует решение ORCH-116, влияющее на весь оркестратор: вводит новый компонент-leaf src/test_runner.py, снимает вторую avoidable LLM-консультацию из потока управления (tester/result:, A5) и переводит rank-2 determinization-roadmap из «план» в «реализовано». Локальная детализация (все решения D1D12, включая tester-специфичную анти-коллизию status: D6.1) — docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md.

Статус

Proposed

Контекст

ORCH-118 (adr-0047) зафиксировал нормативную политику и карту LLM-консультаций и назвал avoidable LLM control paths = {tester, deployer}. Первый срез — deployer (staging-status, rank 1) — реализован ORCH-115 (adr-0048). Второй кандидат — tester (rank 2, needs-hybrid-fallback, hybrid_needed = yes, first_slice = no). ORCH-116 — его фактическая реализация.

Вердикт result: на стадии testing сейчас эмитит LLM-агент tester, но PASS/FAIL-ядро есть чистый маппинг exit-кода pytest + read-only smoke, а гейт check_tests_passed (_parse_tests_verdict) детерминирован и читает только frontmatter result: (+ legacy verdict:/status:). Это удовлетворяет обоим условиям «avoidable»: C-консультация и деривируемый вердикт. Гибрид-нюанс: прежний промпт нёс ещё и настоящее суждение (триаж падений, маппинг TC↔критерии) — поэтому ORCH-116 выносит из потока управления только PASS/FAIL-исполнителя, оставляя LLM допустимым лишь как будущий off-control-path триаж (Phase 2, не control-path).

Прецедент детерминированной замены агента (launch_job-перехват до _spawn, D1/D2 + рабочий эталон src/staging_runner.py ORCH-115) и эталон «детерминированный джоб → advance_stage» уже в проде — архитектурный риск замены снят.

Решение

Новый leaf src/test_runner.py + перехват в launch_job до _spawn (рядом с D1/D2/ORCH-115). На testing для in-scope репо с резолвимым тест-контрактом джоб tester обрабатывает раннер: исполняет регресс pytest <target> в worktree ветки через proc_group (tree-kill, ORCH-110) + опциональный read-only smoke, маппит exit-код единым контрактом self_deploy.map_exit_code_to_status (транслируя токены в PASS/FAIL), пишет 13-test-report.md (тот же machine-key result:), best-effort пушит лог в фичеветку, вызывает существующий advance_stage(current_stage="testing", finished_agent="tester").

Кросс-каттинговые инварианты (сохранены байт-в-байт):

  • STAGE_TRANSITIONS (src/stages.py), реестр и имена QG_CHECKS/check_tests_passed/ _parse_tests_verdict/прочих check_*/_parse_*, machine-verdict-ключи (result:/verdict:/ status:/staging_status:/deploy_status:/security_status:/coverage_status:), схема БД — не тронуты. Это замена продюсера артефакта, не гейта/стадии.
  • Единственный транспорт LLM-консультации (launcher._spawn/S0, llm-usage-policy.md §5) — соблюдён: раннер не зовёт LLM; второй транспорт не вводится; будущий off-control-path триаж — вне control-path (не контр-пример политике).
  • Сквозной бюджет времени ORCH-065/109/110 (reaper_max_running_s (5400) > Σ(работ на ребре)) — соблюдён без правки reaper_max_running_s: ребро testing отдельно от deploy-staging, окно раннера ≤900s ≤ прежнего LLM-окна agent_timeout_seconds (1800s).
  • Граница ORCH-112/ORCH-114/ORCH-115: transition-lease берётся внутри advance_stage; раннер lease/гигиену/staging_runner не модифицирует.

Скоуп — self-hosting only (test_runner_repos=""is_self_hosting_repo + резолв тест-контракта _has_test_contract, в Phase 1 = self-hosting), под kill-switch test_runner_enabled (off → _spawn LLM-tester'а байт-в-байт). never-raise во всех публичных функциях; двухуровневый исход (verdict при исполнившейся сюите; bounded defer → fail-closed на tool-error/таймауте) убирает с testing-ребра RCA-класс ORCH-110 (инфра ≠ код-фейл). Backward-compat (BR-9): репо без резолвимого тест-контракта → applies==False → прежний LLM-tester (enduro-trails не затронут).

Tester-специфичная анти-коллизия (D6.1 локального ADR, отсутствует в ORCH-115): _parse_tests_verdict читает вердикт из трёх полей (verdict:/status:/result:) с negative-token-priority — поэтому обязательное 52c-поле status: раннера жёстко выровнено по вердикту (success для PASS / failed для FAIL), иначе негативный токен в status: при result: PASS дал бы ложный FAIL. Зафиксировано unit-тестом через неизменённый парсер.

Эволюция карты LLM (норматив сопровождения, в том же PR — D12 локального ADR): llm-call-sites.md (A5 → реализовано детерминированно, но avoidable=yes/axis=C/ needs-hybrid-fallback сохранены — LLM-ветвь как fallback / будущий off-control-path триаж), llm-determinization-roadmap.md (rank 2 tester → реализован; инвариант «ровно один first_slice = yes» целfirst_slice остаётся у rank 1/deployer, у tester — no), llm-usage-policy.md (§5 — транспорт не нарушен), плюс анти-дрейф-тесты (test_llm_call_site_inventory.py/test_llm_determinization_docs.py). Эти правки коуплены к коду → применяются в development атомарно с реализацией, не в architecture-стадии (как ORCH-115).

Последствия

  • + Минус ещё один avoidable LLM control path; второй доказанный раннер-паттерн (теперь и для needs-hybrid-fallback-кандидата, не только replace-deterministic-now).
  • + Дешевле/быстрее/детерминированнее собственный testing; нет токенов/латентности LLM в точке ветвления testing → deploy-staging / testing → development.
  • + Паттерн остаётся переиспользуемым: leaf + перехват до _spawn + advance_stage — шаблон для Phase 2 (project test contract не-self репо + опциональный off-control-path LLM-триаж).
  • + Гибрид-граница (D11 локального ADR): архитектура не закрывает будущий off-control-path триаж, не пуская LLM обратно в поток управления вердикта.
  • Новый компонент + врезка + defer-механика + tester-специфичная анти-коллизия status:. Митигейшн: never-raise leaf, kill-switch (fail-safe к LLM), без схемы БД, инвариант выравнивания status: + структурное покрытие tests/test_orch116_test_runner.py.
  • Откат: ORCH_TEST_RUNNER_ENABLED=false → прежний LLM-путь на testing байт-в-байт.

Ссылки

  • Локальный ADR: docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md
  • Первый срез: adr-0048 (ORCH-115, src/staging_runner.py)
  • Политика/карта/roadmap: llm-usage-policy.md, llm-call-sites.md (A5), llm-determinization-roadmap.md (rank 2), adr-0047
  • Прецеденты: D1/D2 (launcher.py:397/402), _run_staging_runner_job (launcher.py:438), run_staging_gate (staging_runner.py), proc_group (ORCH-110, adr-0042), transition-lease (ORCH-114, adr-0045)