116 lines
9.9 KiB
Markdown
116 lines
9.9 KiB
Markdown
---
|
||
work_item: ORCH-116
|
||
stage: architecture
|
||
author_agent: architect
|
||
status: proposed
|
||
created_at: 2026-06-16
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# adr-0049: Детерминированный test-раннер — второй реализованный срез determinization-roadmap (tester-гибрид)
|
||
|
||
> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-116, влияющее на **весь**
|
||
> оркестратор: вводит новый компонент-leaf `src/test_runner.py`, снимает вторую avoidable
|
||
> LLM-консультацию из потока управления (`tester`/`result:`, A5) и переводит rank-2
|
||
> determinization-roadmap из «план» в «реализовано». Локальная детализация (все решения
|
||
> D1–D12, включая tester-специфичную анти-коллизию `status:` D6.1) —
|
||
> `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`.
|
||
|
||
## Статус
|
||
Proposed
|
||
|
||
## Контекст
|
||
|
||
ORCH-118 ([adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)) зафиксировал нормативную
|
||
политику и карту LLM-консультаций и назвал **avoidable LLM control paths = `{tester, deployer}`**.
|
||
Первый срез — **deployer (staging-status, rank 1)** — реализован **ORCH-115**
|
||
([adr-0048](adr-0048-deterministic-staging-runner.md)). Второй кандидат — **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](../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](adr-0048-deterministic-staging-runner.md) (ORCH-115, `src/staging_runner.py`)
|
||
- Политика/карта/roadmap: [llm-usage-policy.md](../llm-usage-policy.md),
|
||
[llm-call-sites.md](../llm-call-sites.md) (A5),
|
||
[llm-determinization-roadmap.md](../llm-determinization-roadmap.md) (rank 2),
|
||
[adr-0047](adr-0047-llm-usage-policy-and-call-site-map.md)
|
||
- Прецеденты: 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](adr-0042-merge-gate-retest-infra-tolerance-and-tree-kill.md)),
|
||
transition-lease (ORCH-114, [adr-0045](adr-0045-transition-ownership-lease-and-stage-cas.md))
|