diff --git a/.env.example b/.env.example index a4d4bb1..ebe44ca 100644 --- a/.env.example +++ b/.env.example @@ -585,6 +585,34 @@ ORCH_STAGING_RUNNER_INFRA_MAX_RETRIES=2 ORCH_STAGING_RUNNER_INFRA_RETRY_DELAY_S=30 ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=true +# ORCH-116: deterministic test-runner replacing the LLM `tester` agent on the +# `testing` stage for the self-hosting orchestrator (2nd determinization slice, +# mirror of the ORCH-115 staging-runner). A leaf src/test_runner.py is intercepted +# in launch_job BEFORE _spawn: it runs the SAME regression `python -m pytest ` +# in the task worktree (+ optional read-only smoke), maps the exit-code -> result: +# PASS|FAIL, writes 13-test-report.md and initiates the UNCHANGED check_tests_passed +# gate. Replaces only the producer of the artifact; the gate / STAGE_TRANSITIONS / DB +# schema are byte-for-byte unchanged. See ADR-001-deterministic-test-runner.md / adr-0050. +# TEST_RUNNER_ENABLED -> kill-switch; false -> the prior LLM tester runs on +# testing via _spawn 1:1. +# TEST_RUNNER_REPOS -> CSV scope; empty -> self-hosting only. A repo with +# no resolvable test-contract is never intercepted (BR-9). +# TEST_RUNNER_TARGET -> pytest target of the test-contract (default tests/). +# TEST_RUNNER_TIMEOUT_S -> wall-clock budget for the pytest regression +# (malformed/non-positive -> default 900 + WARNING). +# TEST_RUNNER_SMOKE_ENABLED -> optional read-only smoke (/health,/status,/queue + +# serial_gate block); false -> pytest exit-code is the sole signal. +# TEST_RUNNER_INFRA_MAX_RETRIES -> tool-error (suite did NOT execute) bounded DEFER +# budget before a fail-closed FAIL (anti ORCH-110). +# TEST_RUNNER_INFRA_RETRY_DELAY_S-> delay before the re-queued tester job. +ORCH_TEST_RUNNER_ENABLED=true +ORCH_TEST_RUNNER_REPOS= +ORCH_TEST_RUNNER_TARGET=tests/ +ORCH_TEST_RUNNER_TIMEOUT_S=900 +ORCH_TEST_RUNNER_SMOKE_ENABLED=true +ORCH_TEST_RUNNER_INFRA_MAX_RETRIES=2 +ORCH_TEST_RUNNER_INFRA_RETRY_DELAY_S=30 + # ORCH-057 (follow-up ORCH-040): legacy root-owned ownership detect + actionable # worktree error. After the uid migration (user: "1000:1000") legacy root:root files # in /repos broke worktree creation under uid 1000 with a raw "Permission denied". diff --git a/CHANGELOG.md b/CHANGELOG.md index b64b721..6c74080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу. ## [Unreleased] +- **Детерминированный test-раннер вместо LLM-тестера на `testing`** (ORCH-116, `feat`): второй реализованный срез determinization-roadmap (ORCH-118 A5, `needs-hybrid-fallback`) — на стадии `testing` для self-hosting `orchestrator` **LLM-агент `tester` заменён детерминированным кодом** (`src/test_runner.py`). PASS/FAIL-ядро агента было деривируемым (регресс `pytest` + read-only smoke → `result:`); каждый прогон жёг токены/время opus-агента (~60–150k / 5–20 мин) и встраивал недетерминизм LLM в точку ветвления `testing → deploy-staging` / `testing → development`. **Инвариант (NFR-1):** это замена *продюсера* артефакта, **не** гейта — контракт `13-test-report.md`, гейт `check_tests_passed`/`_parse_tests_verdict`, `STAGE_TRANSITIONS`, machine-verdict `result:` (+ legacy `verdict:`/`status:`), схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, скоуп self-hosting, гибрид (LLM строго off-control-path). Эталон — `src/staging_runner.py` (ORCH-115). ADR: `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной `docs/architecture/adr/adr-0050-deterministic-test-runner.md`. + - **Перехват в `launch_job` до `_spawn` (D1):** `if job.agent=="tester" and test_runner.should_intercept(job)` → `_run_test_runner_job` (зеркало `_run_staging_runner_job`, прецедент `deploy-finalizer`/`post-deploy-monitor`/`staging-runner` `launcher.py:397/402/405`): синхронно ведёт `jobs`-строку через `mark_job`, возвращает `None` (нет `agent_runs`, нет токенов). Дискриминатор — роль `tester` **И** стадия задачи `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`, коллизии стадий нет, в отличие от общей роли `deployer`) **И** `applies(repo)`; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути). + - **Leaf `src/test_runner.py` (новый, чистый never-raise):** по образцу `staging_runner`/`self_deploy`/`proc_group` (на импорте только `config`/`proc_group`; `db`/`git_worktree`/`self_deploy`/`qg.checks`/`stage_engine`/`notifications` — лениво). `applies(repo)` = kill-switch `test_runner_enabled` + скоуп `test_runner_repos` (пусто → self-hosting only) **И** резолв тест-контракта `_has_test_contract` (BR-9: репо без контракта → `False` → LLM-tester — enduro-trails 1:1 как до ORCH-116, даже если руками добавлен в CSV). Исполняет регресс `python -m pytest ` **в worktree ветки** (`git_worktree.get_worktree_path`, анти checkout-гонка ORCH-112) через `proc_group.run_in_process_group` (tree-kill, таймаут `test_runner_timeout_s=900`, малформ/непозитив → дефолт + WARNING) + опц. **read-only smoke** (`/health`/`/status`/`/queue` + блок `serial_gate`, stdlib `urllib`; транзиентная недостижимость — ограниченный ретрай, не-200/нет блока — немедленный FAIL; `test_runner_smoke_enabled`). Маппит exit-код **единым** контрактом `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе/None→`FAIL`, fail-closed; smoke-провал AND-ится в `FAIL`); пишет `13-test-report.md` (тот же machine-key `result:` UPPERCASE + 52c-схема, `author_agent: test-runner`/`model_used: n/a`) + best-effort push в **фичеветку**; вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")` — без новых рёбер/исходов (transition-lease ORCH-114 берётся внутри `advance_stage` — граница O1). + - **Анти-коллизия 52c-`status:` ↔ парсер (D6.1, специфично для tester):** `_parse_tests_verdict` читает вердикт из **трёх** равноранговых полей (`verdict:`/`status:`/`result:`) с negative-token-priority. 52c-обязательное `status:` поэтому читается тем же парсером → раннер **ВСЕГДА выравнивает** `status:` по вердикту (`PASS → status: success`, `FAIL → status: failed`) — иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL здорового прогона. Прибито unit-тестом через **неизменённый** парсер. + - **Двухуровневый исход (D5, анти-ORCH-110):** сюита **исполнилась** (реальный exit-код) → verdict→advance (FAIL → тот же откат `testing → development` + developer-retry, что у FAIL-вердикта LLM, `stage_engine.py:849`); сюита **не исполнилась** (tool-error: spawn-error/таймаут/`returncode None`) → инфра-сбой ≠ код-фейл → bounded **DEFER** (re-queue **`tester`**-джоба с задержкой + restart-safe маркер `test-runner infra-retry` в `task_content`, счётчик подсчётом маркера — без правки схемы БД), на исчерпании `test_runner_infra_max_retries=2` → fail-closed `FAIL` + advance + INFRA-alert. Раннер **никогда** не делает тихий advance/ложный green, **никогда** не клинит очередь, **не** жжёт developer-retry на транзиентной инфре. + - **Гибрид (D11/BR-8/NFR-7):** в Phase 1 на `testing` (in-scope) вердикт `result:` производит **только** детерминированный раннер; LLM **не** вызывается в потоке управления вердикта. Архитектура не запрещает будущий **off-control-path** LLM-триаж падений / маппинг TC↔критерии после детерминированного FAIL (отдельная роль/джоб, **не** выносит и **не** переопределяет `result:`, **не** добавляет ребро в `STAGE_TRANSITIONS`) — в этом срезе не реализуется. Self-hosting safety: в командах раннера нет рестарта 8500 / `docker compose up orchestrator` / `--build` / force-push / правок `.env`; smoke строго read-only. Наблюдаемость — in-process счётчики (`runs`/`pass`/`fail`/`tool_error`/`deferred`) + read-only блок `test_runner` в `GET /queue` + один структурный лог-вердикт на прогон (различает код-фейл и tool-error). Флаги (`config.py`, дефолт = боевое): `test_runner_enabled`/`test_runner_repos`/`test_runner_target`/`test_runner_timeout_s`/`test_runner_smoke_enabled`/`test_runner_infra_max_retries`/`test_runner_infra_retry_delay_s` (env `ORCH_TEST_RUNNER_*`). Откат = `ORCH_TEST_RUNNER_ENABLED=false` → на `testing` снова LLM-`tester` через `_spawn` **байт-в-байт**. + - **Норматив сопровождения ORCH-118 (NFR-6):** обновлены `docs/architecture/llm-call-sites.md` (A5 — реализован; машинный `ORCH-118-INVENTORY-BLOCK` сохраняет tester как `avoidable=yes`/`axis=C`/`needs-hybrid-fallback`), `llm-determinization-roadmap.md` (rank 2 tester — ✅ реализован; инвариант «ровно один `first_slice = yes`» у rank 1 deployer цел), `llm-usage-policy.md` (§5 — единственный транспорт S0 не нарушен, раннер LLM не зовёт), `.openclaw/agents/tester.md` (LLM-ветвь `testing` — fallback), `docs/architecture/README.md`/`internals.md`, витрина `docs/overview/tech-pipeline.md`/`tech-agents.md`/`tech-quality-security.md`. Покрытие — `tests/test_orch116_test_runner.py` (TC-01…TC-14) + зелёные `tests/test_llm_call_site_inventory.py`/`test_llm_determinization_docs.py` (TC-15). - **Host-side исполнение staging-раннера + классификация environment-дефекта** (ORCH-123, `fix`, bug→escalate full-cycle): устранён инцидент **ORCH-116** — детерминированный staging-раннер (ORCH-115) вызывал `docker exec` **изнутри** прод-контейнера `orchestrator`, где **нет бинаря `docker`** (образ несёт только `openssh-client git curl`, `Dockerfile:11`; `/var/run/docker.sock` смонтирован, но клиента нет) → `Popen(["docker", …])` падал `FileNotFoundError` → ветка tool-error → инфра-DEFER×2 → fail-closed `FAILED` → **ложный** откат `deploy-staging → development` (как код-фейл, с расходом developer-retry). Так до фикса **любая** self-hosting задача, дойдя до `deploy-staging`, была обречена на ложный откат. Аддитивно, под флагами, never-raise, скоуп self-hosting; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_staging_status`/`_parse_staging_status` / machine-verdict-ключи (`staging_status:`/`deploy_status:`) / схема БД — **байт-в-байт не тронуты** (замена *стратегии исполнения продюсера* `15-staging-log.md`, **не** гейта/стадии; зеркало инварианта ORCH-115 NFR-1). ADR: `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`, сквозной `docs/architecture/adr/adr-0049-host-side-docker-execution-boundary.md`. - **Host-side ssh-стратегия (D1):** `staging_runner.build_staging_command()` теперь обёртывает ту же `docker exec orchestrator-staging python3 … staging_check.py … --mode stub` в `ssh -o StrictHostKeyChecking=no @ ''` (зеркало `self_deploy.build_deploy_command` / `image_freshness.image_revision(ssh_target=…)`); канал — существующий доверенный (`ORCH_DEPLOY_SSH_HOST=127.0.0.1`, ssh-ключ смонтирован `:ro`, `openssh-client` в образе) → **новых секретов/привилегий не вводится** (NFR-3). Меняется **инициатор/канал** запуска, **не** сама сюита (она по-прежнему бежит **внутри** `orchestrator-staging` 8501). **Security (D2):** docker CLI/SDK в контейнер **не добавляется**, `docker.sock` **не используется изнутри** — это было бы root-эквивалентным расширением поверхности атаки (доступным и LLM-агентам); host-side ssh достигает цели без расширения привилегий. - **Трёхсторонняя классификация исхода (D3, чистая `classify_staging_outcome`, зеркало `merge_gate.classify_retest_failure`):** `suite-ran` (распознанный exit-код, кроме 255, **без** env-маркера в stderr → доверяем коду: `0→SUCCESS`/`≠0→FAILED`; анти-over-tolerance BR-3 — реальный фейл сюиты **никогда** не реклассифицируется в инфру), `permanent-env` (spawn-error `rc=None` без таймаута / нет ssh-target / `rc∈{126,127}` / env-маркер `No such container`/`Cannot connect to the Docker daemon`/`command not found` → ретрай бессмыслен), `transient-infra` (timeout / ssh transport `rc=255` / неизвестный сигнал → ретрай осмыслен). Дизамбигуация коллизии `exit=1` (`docker exec` «No such container»=1 vs суита fail=1) — **скан stderr на env-маркеры**, не голый exit-код; fail-safe при неоднозначности → `transient-infra` (DEFER), никогда тихий `suite-ran`. diff --git a/CLAUDE.md b/CLAUDE.md index da41afd..0b6809b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -548,7 +548,7 @@ Plane, **не** Quality Gate и **не** стадия). целы) + `tester.md` (LLM-ветвь — fallback). Покрытие — `tests/test_orch116_test_runner.py` (TC-01…TC-14) + зелёные LLM-анти-дрейф тесты (TC-15). Детали — `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной - `docs/architecture/adr/adr-0049-deterministic-test-runner.md`. + `docs/architecture/adr/adr-0050-deterministic-test-runner.md`. ## Машинный журнал уроков (ORCH-098) Шаг 1 («Фундамент», F2) эпика саморазвития: формализует свободнотекстовые «уроки» из `memory/` в diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 171e987..4f41179 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -11,7 +11,7 @@ - **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`. - **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. **ORCH-109 ([adr-0040](adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md)):** (1) резолвенная **модель стампится в `agent_runs.model` в момент launch** (`_spawn`, объединённый `UPDATE … SET model=?, effort=?` рядом со стампом эффорта ORCH-087; пустой резолв → `NULL`; never-raise) → модель видна не-`null` при любом исходе прогона, включая timeout-kill (`exit_code=-9`), и in-flight в `GET /metrics`/`GET /queue` (`get_running_agents` уже отдаёт `model`); постфактум `record_usage` (`model=COALESCE(?, model)`) остаётся **обогащением**, не единственным источником истины. (2) **Per-role wall-clock бюджеты** через выделенные ключи `agent_timeout_developer_s=3600`/`agent_timeout_reviewer_s=3000` (лестница `_resolve_timeout`: `agent_timeout_overrides_json` → выделенный ключ роли → `agent_timeout_seconds=1800`; прочие роли — байт-в-байт; малформный/вне-диапазонный конфиг → дефолт + WARNING). Инвариант reaper ORCH-065 сохранён синхронным поднятием `reaper_max_running_s` 3600→**5400** (`5400 > max(timeout)3600 + grace20`). FR-5 анти-salvage — структурно: продвижение гейтится `if exit_code==0`, timeout-kill → `_finalize_job` (retry/fail), не advance. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты. - **Staging-runner** (`src/staging_runner.py`, ORCH-115 — [adr-0048](adr/adr-0048-deterministic-staging-runner.md)) — чистый **never-raise** leaf (паттерн `self_deploy`/`proc_group`/`staging_verdict`), заменяющий **LLM-агента `deployer` на стадии `deploy-staging`** детерминированным кодом (первый реализованный срез determinization-roadmap, A6/`replace-deterministic-now`). Перехват в `launch_job` **до `_spawn`** (рядом с D1/D2 `deploy-finalizer`/`post-deploy-monitor`): дискриминатор — **стадия задачи** `deploy-staging` **И** `applies(repo)` (kill-switch `staging_runner_enabled` + скоуп `staging_runner_repos`, пусто → self-hosting only), `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM). Раннер (зеркало `run_deploy_finalizer`) исполняет ту же staging-сюиту (`docker exec orchestrator-staging … staging_check.py`) через `proc_group` (tree-kill, таймаут `staging_runner_timeout_s=600`), маппит exit-код единым контрактом `self_deploy.map_exit_code_to_status` (`0→SUCCESS`/иначе→`FAILED`; ORCH-061 infra-tolerance остаётся внутри `staging_check.py`), пишет `15-staging-log.md` (тот же machine-key `staging_status:`, `author_agent: staging-runner`/`model_used: n/a`) + best-effort push в фичеветку, и вызывает **существующий** `advance_stage(current_stage="deploy-staging", finished_agent="deployer")` — без новых рёбер/исходов; transition-lease ORCH-114 берётся внутри `advance_stage` (раннер не трогает). **Двухуровневый исход (анти-ORCH-110):** сюита исполнилась → verdict→advance (FAILED → тот же откат `deploy-staging → development` + developer-retry, что у LLM); tool-error/таймаут → bounded defer (`staging_runner_infra_max_retries`) → fail-closed `FAILED` + alert на исчерпании (инфра ≠ код-фейл, никогда тихий advance/ложный green). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_staging_status`/`_parse_staging_status`/machine-verdict/схема БД — **байт-в-байт не тронуты** (замена *продюсера*, не гейта). Наблюдаемость — in-process счётчики + блок `staging_runner` в `GET /queue`. Откат — `ORCH_STAGING_RUNNER_ENABLED=false` (прежний LLM-deployer-путь байт-в-байт). Детали — `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md`. **ORCH-123 (фикс стратегии исполнения — [adr-0049](adr/adr-0049-host-side-docker-execution-boundary.md)):** раннер ORCH-115 вызывал `docker exec` **изнутри** прод-контейнера, где **нет docker CLI** (образ несёт только `openssh-client git curl`, `Dockerfile:11`) → `FileNotFoundError` → постоянный environment-дефект ложно откатывался как код-фейл `deploy-staging → development` (инцидент ORCH-116). Фикс: `build_staging_command` теперь исполняет ту же `docker exec … staging_check.py … --mode stub` **host-side** через доверенный ssh-канал `ssh ''` (зеркало `self_deploy`/`image_freshness`, флаг `ORCH_STAGING_RUNNER_EXEC_HOST_SIDE=true`, дефолт; меняется **инициатор/канал**, не сама сюита — она по-прежнему бежит внутри `orchestrator-staging` 8501). Двухуровневый исход ORCH-115 заменён **трёхсторонней классификацией** (`classify_staging_outcome`): `suite-ran` (распознанный exit-код без env-маркера в stderr → доверяем коду: `0→SUCCESS`/`≠0→FAILED`+прежний откат, анти-over-tolerance), `permanent-env` (spawn-error/нет ssh-target/126-127/env-маркер `No such container`/`Cannot connect to the Docker daemon` → **немедленный infra-HOLD**: без отката, без developer-retry, DEFER пропускается), `transient-infra` (timeout/ssh 255 → bounded DEFER, на исчерпании → infra-HOLD, **не** прежний fail-closed FAILED+откат). Корневой инвариант: «сюита **не** исполнилась» (environment ИЛИ инфра) **никогда** не оканчивается код-фейл-откатом — закрывает RCA-класс ORCH-110 на staging-ребре. Plus prod-like preflight host-side канала в `main.lifespan` (best-effort, never-blocks). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_staging_status`/machine-verdict/схема БД — байт-в-байт не тронуты. Детали — `docs/work-items/ORCH-123/06-adr/ADR-001-host-side-staging-execution-and-env-classification.md`. -- **Test-runner** (`src/test_runner.py`, ORCH-116 — [adr-0049](adr/adr-0049-deterministic-test-runner.md)) — чистый **never-raise** leaf (зеркало `staging_runner`), заменяющий **LLM-агента `tester` на стадии `testing`** детерминированным кодом (**второй** реализованный срез determinization-roadmap, A5/`needs-hybrid-fallback`). Перехват в `launch_job` **до `_spawn`** (рядом с D1/D2/ORCH-115): дискриминатор — роль `tester` **И** стадия задачи `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`) **И** `applies(repo)` (kill-switch `test_runner_enabled` + скоуп `test_runner_repos`, пусто → self-hosting only, **И** резолв тест-контракта `_has_test_contract` — репо без контракта → LLM-tester, BR-9). `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM). Раннер (зеркало `run_staging_gate`) исполняет регресс `python -m pytest ` **в worktree ветки** (анти checkout-гонка) через `proc_group` (tree-kill, таймаут `test_runner_timeout_s=900`) + опц. **read-only smoke** (`/health`/`/status`/`/queue` + блок `serial_gate`, `test_runner_smoke_enabled`; транзиентная недостижимость — ограниченный ретрай, не-200/нет блока — немедленный FAIL), маппит exit-код единым контрактом `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе→`FAIL`; smoke-провал AND-ится в `FAIL`), пишет `13-test-report.md` (тот же machine-key `result:`, `author_agent: test-runner`/`model_used: n/a`; **52c-`status:` ВСЕГДА выровнен по вердикту** `success`/`failed` — иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL через `_parse_tests_verdict`, D6.1) + best-effort push в фичеветку, и вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")` — без новых рёбер/исходов; transition-lease ORCH-114 берётся внутри `advance_stage` (раннер не трогает). **Двухуровневый исход (анти-ORCH-110):** сюита исполнилась → verdict→advance (FAIL → тот же откат `testing → development` + developer-retry, что у LLM); tool-error/таймаут (сюита НЕ исполнилась) → bounded defer (re-queue **`tester`**-джоба, `test_runner_infra_max_retries`) → fail-closed `FAIL` + INFRA-alert на исчерпании (инфра ≠ код-фейл, никогда тихий advance/ложный green, не жжёт developer-retry на транзиентной инфре). **Гибрид (BR-8/NFR-7):** LLM строго off-control-path — детерминированный раннер единственный продюсер `result:`; будущий off-control-path триаж падений не выносит/не переопределяет вердикт и не добавляет ребро в `STAGE_TRANSITIONS` (в Phase 1 не реализован). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_tests_passed`/`_parse_tests_verdict`/machine-verdict/схема БД — **байт-в-байт не тронуты** (замена *продюсера*, не гейта). Наблюдаемость — in-process счётчики + блок `test_runner` в `GET /queue`. Откат — `ORCH_TEST_RUNNER_ENABLED=false` (прежний LLM-tester-путь байт-в-байт). Детали — `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`. +- **Test-runner** (`src/test_runner.py`, ORCH-116 — [adr-0050](adr/adr-0050-deterministic-test-runner.md)) — чистый **never-raise** leaf (зеркало `staging_runner`), заменяющий **LLM-агента `tester` на стадии `testing`** детерминированным кодом (**второй** реализованный срез determinization-roadmap, A5/`needs-hybrid-fallback`). Перехват в `launch_job` **до `_spawn`** (рядом с D1/D2/ORCH-115): дискриминатор — роль `tester` **И** стадия задачи `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`) **И** `applies(repo)` (kill-switch `test_runner_enabled` + скоуп `test_runner_repos`, пусто → self-hosting only, **И** резолв тест-контракта `_has_test_contract` — репо без контракта → LLM-tester, BR-9). `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM). Раннер (зеркало `run_staging_gate`) исполняет регресс `python -m pytest ` **в worktree ветки** (анти checkout-гонка) через `proc_group` (tree-kill, таймаут `test_runner_timeout_s=900`) + опц. **read-only smoke** (`/health`/`/status`/`/queue` + блок `serial_gate`, `test_runner_smoke_enabled`; транзиентная недостижимость — ограниченный ретрай, не-200/нет блока — немедленный FAIL), маппит exit-код единым контрактом `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе→`FAIL`; smoke-провал AND-ится в `FAIL`), пишет `13-test-report.md` (тот же machine-key `result:`, `author_agent: test-runner`/`model_used: n/a`; **52c-`status:` ВСЕГДА выровнен по вердикту** `success`/`failed` — иначе негативный токен в `status:` при `result: PASS` дал бы ложный FAIL через `_parse_tests_verdict`, D6.1) + best-effort push в фичеветку, и вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")` — без новых рёбер/исходов; transition-lease ORCH-114 берётся внутри `advance_stage` (раннер не трогает). **Двухуровневый исход (анти-ORCH-110):** сюита исполнилась → verdict→advance (FAIL → тот же откат `testing → development` + developer-retry, что у LLM); tool-error/таймаут (сюита НЕ исполнилась) → bounded defer (re-queue **`tester`**-джоба, `test_runner_infra_max_retries`) → fail-closed `FAIL` + INFRA-alert на исчерпании (инфра ≠ код-фейл, никогда тихий advance/ложный green, не жжёт developer-retry на транзиентной инфре). **Гибрид (BR-8/NFR-7):** LLM строго off-control-path — детерминированный раннер единственный продюсер `result:`; будущий off-control-path триаж падений не выносит/не переопределяет вердикт и не добавляет ребро в `STAGE_TRANSITIONS` (в Phase 1 не реализован). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_tests_passed`/`_parse_tests_verdict`/machine-verdict/схема БД — **байт-в-байт не тронуты** (замена *продюсера*, не гейта). Наблюдаемость — in-process счётчики + блок `test_runner` в `GET /queue`. Откат — `ORCH_TEST_RUNNER_ENABLED=false` (прежний LLM-tester-путь байт-в-байт). Детали — `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`. - **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`. - **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы существующих таблиц — байт-в-байт. Подробнее ниже (§ «Единое владение side-effectful переходами»). Детали — `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`. @@ -449,7 +449,7 @@ ORCH-079 синхронизирует витрину с кодом и закры `llm-call-sites.md` (A6), `llm-determinization-roadmap.md` (rank 1, инвариант «ровно один `first_slice`») и анти-дрейф-тесты обновляются **атомарно с кодом** в development (норматив сопровождения NFR-6). -- **Второй срез реализован (ORCH-116 — [adr-0049](adr/adr-0049-deterministic-test-runner.md)):** +- **Второй срез реализован (ORCH-116 — [adr-0050](adr/adr-0050-deterministic-test-runner.md)):** rank-2 кандидат **tester (`result:`)** заменён детерминированным `src/test_runner.py` (тем же перехватом до `_spawn`) — A5 переходит из «план» в «реализовано». `needs-hybrid-fallback`-природа сохранена: детерминированное ядро (exit-код `pytest` + smoke) вынесено в раннер, LLM-ветвь жива как diff --git a/docs/architecture/adr/adr-0049-deterministic-test-runner.md b/docs/architecture/adr/adr-0050-deterministic-test-runner.md similarity index 99% rename from docs/architecture/adr/adr-0049-deterministic-test-runner.md rename to docs/architecture/adr/adr-0050-deterministic-test-runner.md index c1c878a..5f20742 100644 --- a/docs/architecture/adr/adr-0049-deterministic-test-runner.md +++ b/docs/architecture/adr/adr-0050-deterministic-test-runner.md @@ -7,7 +7,7 @@ created_at: 2026-06-16 model_used: claude-opus-4-8 --- -# adr-0049: Детерминированный test-раннер — второй реализованный срез determinization-roadmap (tester-гибрид) +# adr-0050: Детерминированный test-раннер — второй реализованный срез determinization-roadmap (tester-гибрид) > **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-116, влияющее на **весь** > оркестратор: вводит новый компонент-leaf `src/test_runner.py`, снимает вторую avoidable diff --git a/docs/architecture/internals.md b/docs/architecture/internals.md index 43f905b..0328e77 100644 --- a/docs/architecture/internals.md +++ b/docs/architecture/internals.md @@ -109,7 +109,7 @@ LLM). Leaf `src/staging_runner.py` (зеркало `run_deploy_finalizer`) ис `self_deploy.map_exit_code_to_status`, пишет `15-staging-log.md` (тот же machine-key `staging_status:`) и вызывает существующий `advance_stage(finished_agent="deployer")` (см. §5). Так LLM-агент `deployer` на `deploy-staging` исчезает из happy-path; `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты. -**ORCH-116 ([adr-0049](adr/adr-0049-deterministic-test-runner.md)):** тем же паттерном (рядом с +**ORCH-116 ([adr-0050](adr/adr-0050-deterministic-test-runner.md)):** тем же паттерном (рядом с ORCH-115) перехватывается джоб `tester` на стадии `testing` для in-scope репо с тест-контрактом (дискриминатор — роль `tester` **И** `tasks.stage == "testing"` **И** `test_runner.applies(repo)` под kill-switch `test_runner_enabled`, скоуп `test_runner_repos`, резолв `_has_test_contract`; пусто → diff --git a/docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md b/docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md index 2f682fd..9702a92 100644 --- a/docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md +++ b/docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md @@ -13,7 +13,7 @@ Work Item: **ORCH-116** — заменить LLM-агента `tester` на ст (self-hosting `orchestrator`) детерминированным test-раннером (второй срез determinization-roadmap, **rank 2 / tester-гибрид**). Стадия: **architecture** -Сквозная регистрация: **`docs/architecture/adr/adr-0049-deterministic-test-runner.md`** +Сквозная регистрация: **`docs/architecture/adr/adr-0050-deterministic-test-runner.md`** (решение кросс-каттинговое — вводит новый компонент-leaf `src/test_runner.py` и реализует второй срез determinization-roadmap; влияет на карту LLM-консультаций всего оркестратора). @@ -459,7 +459,7 @@ LLM-консультации; политика соблюдена. - Тест-план: `docs/work-items/ORCH-116/04-test-plan.yaml` - Инфра: `docs/work-items/ORCH-116/07-infra-requirements.md`; Данные: `08-data-requirements.md`; Риски: `10-tech-risks.md` -- Сквозной ADR: `docs/architecture/adr/adr-0049-deterministic-test-runner.md` +- Сквозной ADR: `docs/architecture/adr/adr-0050-deterministic-test-runner.md` - Эталон реализации: `src/staging_runner.py` (ORCH-115), `docs/work-items/ORCH-115/06-adr/ADR-001-deterministic-staging-runner.md` - Сверено по коду: `src/agents/launcher.py:397/404/438`, `src/stages.py:17-18`, diff --git a/docs/work-items/ORCH-116/12-review.md b/docs/work-items/ORCH-116/12-review.md index edd1868..c9a8af1 100644 --- a/docs/work-items/ORCH-116/12-review.md +++ b/docs/work-items/ORCH-116/12-review.md @@ -23,7 +23,7 @@ leaf `src/test_runner.py`, перехватываемым в `launch_job` **до байт-в-байт: `src/stages.py`, `src/qg/checks.py`, `src/staging_runner.py`, `src/transition_lease.py`, `src/proc_group.py` — **не тронуты** (подтверждено `git diff --stat`); в `src/` изменены только `test_runner.py` (новый), `launcher.py`/`config.py`/`main.py` (аддитивно). Документация = golden -source обновлена в том же PR (8 doc-файлов + CHANGELOG + ADR-001/adr-0049). Полный регресс +source обновлена в том же PR (8 doc-файлов + CHANGELOG + ADR-001/adr-0050). Полный регресс **проверен лично**: `pytest tests/ -q` → **2137 passed**; ORCH-116 + LLM-анти-дрейф + system-docs → **74 passed**. **P0/P1/P2 findings отсутствуют → `APPROVED`.** @@ -68,7 +68,7 @@ source обновлена в том же PR (8 doc-файлов + CHANGELOG + AD ### 2. Соответствие ADR — PASS -Реализация дословно следует ADR-001 (D1–D12) и сквозному adr-0049. Сверено критическое: +Реализация дословно следует ADR-001 (D1–D12) и сквозному adr-0050. Сверено критическое: - **D6.1 (анти-коллизия 52c-`status:` ↔ `_parse_tests_verdict`)** — самая тонкая мина задачи. Сверено по **неизменённому** парсеру (`src/qg/checks.py:226-282`): он читает вердикт из ТРЁХ равноранговых полей `verdict:`/`status:`/`result:` c negative-token-priority. `build_test_report` @@ -124,7 +124,7 @@ source обновлена в том же PR (8 doc-файлов + CHANGELOG + AD fallback под выключенным флагом / для репо без контракта»; канон 52d (5 секций, ключ `result:`) цел. - **`CHANGELOG.md`** — развёрнутая запись `[Unreleased]`. - **ADR** заведён (`06-adr/ADR-001-deterministic-test-runner.md` + сквозной - `adr/adr-0049-deterministic-test-runner.md`). + `adr/adr-0050-deterministic-test-runner.md`). Обзорные доки / README «Известные ограничения» (ORCH-079): раннер — **новая способность**, не закрытие документированного ограничения → дополнительных правок README не требуется. @@ -163,7 +163,7 @@ source обновлена в том же PR (8 doc-файлов + CHANGELOG + AD Обновлена в полном объёме в том же PR (см. ось 4): `CLAUDE.md`, `docs/architecture/README.md`, `internals.md`, `llm-call-sites.md`, `llm-determinization-roadmap.md`, `llm-usage-policy.md`, `docs/overview/{tech-pipeline,tech-agents,tech-quality-security}.md`, `.openclaw/agents/tester.md`, -`CHANGELOG.md`, ADR-001 + сквозной adr-0049. Анти-дрейф-тесты документации +`CHANGELOG.md`, ADR-001 + сквозной adr-0050. Анти-дрейф-тесты документации (`test_llm_call_site_inventory.py`, `test_llm_determinization_docs.py`, `test_system_docs.py`) — зелёные (лично прогнаны). Невыполненных требований к обновлению документации **нет**. diff --git a/src/config.py b/src/config.py index b754f5b..d379ce1 100644 --- a/src/config.py +++ b/src/config.py @@ -489,7 +489,7 @@ class Settings(BaseSettings): # STAGE_TRANSITIONS and the DB schema are byte-for-byte UNCHANGED — this only # replaces the *producer* of the artifact. Pattern = staging_runner_* / coverage_gate_*. # See docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md and - # docs/architecture/adr/adr-0049-deterministic-test-runner.md. + # docs/architecture/adr/adr-0050-deterministic-test-runner.md. # test_runner_enabled -> SINGLE kill-switch (env ORCH_TEST_RUNNER_ENABLED). # False -> the intercept never fires -> the prior # LLM tester runs on testing via _spawn byte-for-byte