Compare commits

...

16 Commits

Author SHA1 Message Date
stream
58116f93bd docs(history): сводные уроки вечера 05.06 — ORCH-17/45/47, деплой прода, грабли auth/git/Gitea
All checks were successful
CI / test (pull_request) Successful in 11s
2026-06-05 21:41:52 +00:00
941eec248e Merge pull request 'docs(ORCH-047): staging gate log — FAILED' (#41) from staging-log/ORCH-047-20260605213215 into main 2026-06-06 00:32:38 +03:00
b061354a8f docs(ORCH-047): staging gate log — FAILED (9/10, B6 registry isolation)
All checks were successful
CI / test (pull_request) Successful in 11s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 21:32:15 +00:00
5d04de9eb6 fix(qg): ORCH-047 read result in tests gate (#40)
Manual merge by Owner. check_tests_passed reads result as equal-rank field. APPROVED reviewer v3, 68 tests pass.
2026-06-06 00:25:40 +03:00
edff0484c9 reviewer(ET): auto-commit from reviewer run_id=134
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 11s
2026-06-05 21:18:33 +00:00
2f396452e8 tester(ET): auto-commit from tester run_id=132
All checks were successful
CI / test (push) Successful in 15s
CI / test (pull_request) Successful in 12s
2026-06-05 21:14:05 +00:00
185eb3f6cf reviewer(ET): auto-commit from reviewer run_id=131
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 12s
2026-06-05 21:12:23 +00:00
58fc0a8b94 tester(ET): auto-commit from tester run_id=129
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 12s
2026-06-05 21:07:30 +00:00
c1abfb7436 reviewer(ET): auto-commit from reviewer run_id=128
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 12s
2026-06-05 21:06:01 +00:00
51a76e8169 fix(qg): read result: alongside verdict:/status: in tests gate
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 11s
_parse_tests_verdict now accepts three equal-rank machine-readable
frontmatter fields in 13-test-report.md — result: (canonical tester
output), verdict: and status: (legacy/enduro-trails). Any one non-empty
field suffices; a negative token in any field stays authoritative.

Fixes the producer/consumer contract mismatch where the tester emits
`result: PASS` (per .openclaw/agents/tester.md) but the gate only read
verdict:/status:, causing a testing->development rollback loop until
MAX_DEVELOPER_RETRIES (observed on ORCH-17). Token sets frozen and gate
signature/QG_CHECKS unchanged for full backward compatibility.

Refs: ORCH-047
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 21:03:32 +00:00
75fb4069a4 architect(ET): auto-commit from architect run_id=126
All checks were successful
CI / test (push) Successful in 11s
2026-06-05 21:00:57 +00:00
c3879f2b80 analyst(ET): auto-commit from analyst run_id=125
All checks were successful
CI / test (push) Successful in 12s
2026-06-05 20:44:58 +00:00
974d4f94db docs: init ORCH-047 business request
All checks were successful
CI / test (push) Successful in 13s
2026-06-05 23:42:29 +03:00
982698c4e3 feat(qg): ORCH-045 CI poll-retry (#39)
Some checks failed
CI / test (push) Has been cancelled
Manual merge by Owner (Slava). check_ci_green polling with retry to fix CI race.
2026-06-05 23:08:15 +03:00
stream
0eff781d13 feat(qg): ORCH-045 — poll check_ci_green with retry to fix CI race (pending->success)
All checks were successful
CI / test (push) Successful in 12s
CI / test (pull_request) Successful in 12s
2026-06-05 19:59:06 +00:00
b9c61fc1f1 Merge pull request 'docs: uroki ORCH-017 (deadlock shared-gate, isporchennyy telefon, otkat)' (#38) from docs/lessons-orch-017 into main 2026-06-05 22:50:17 +03:00
18 changed files with 996 additions and 42 deletions

View File

@@ -5,6 +5,7 @@
## [Unreleased]
### Added
- **Поллинг с ретраем в quality-gate `check_ci_green`** (ORCH-045): гейт CI превращён из single-shot в polling, чтобы устранить race condition — раньше один опрос combined commit-status сразу после пуша developer-а ловил транзиентный `pending` (типично 1-3с, реальный кейс ORCH-017: опрос 17:58:54 → pending, CI дозеленел 17:58:55) и задача застревала насмерть без повторного опроса. Теперь: `success` → пропуск сразу; `failure`/`error` → провал сразу (терминально, ретрай бессмыслен); `pending`/unknown → `time.sleep` и повторный опрос до `ci_poll_max_attempts` раз; истечение попыток → явный `(False, "CI still pending after <T>s")` (тупик больше не молчаливый); 404 → как раньше; транзиентная `httpx.HTTPError` на попытке логируется и ретраится в рамках бюджета. Параметры — новые настройки `ORCH_CI_POLL_MAX_ATTEMPTS` (12) и `ORCH_CI_POLL_INTERVAL_S` (10) в `src/config.py` (~2 мин ожидания pending). Сигнатура `check_ci_green(repo, branch)` и реестр `QG_CHECKS` не менялись; `check_tests_passed` не затронут. ADR `docs/architecture/adr/adr-0004-ci-poll-retry.md`. Тесты: `tests/test_qg.py::TestCheckCIGreen`.
- **Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве** (ORCH-017): пингующее сообщение `notify_approve_requested` теперь встраивает две HTML-`<a>`-ссылки — на `docs/work-items/<WI>/01-brd.md` (Gitea branch-view: `gitea_public_url``gitea_url`) и на issue в Plane (`{web_base}/{workspace}/projects/{project_id}/issues/{plane_issue_id}/`). Новая настройка `ORCH_PLANE_WEB_URL` (внешний браузерный web-URL Plane; фолбэк на `plane_api_url`). **Loopback-guard:** если итоговый Plane web-base указывает на localhost/127.0.0.1/0.0.0.0/::1 или пуст — Plane-ссылка опускается (не выпускаем битый localhost-URL). Graceful degradation: каждая ссылка строится независимо и опускается при нехватке данных, сообщение и призыв «Переведите задачу в статус Approved …» сохраняются всегда; ровно одно пингующее сообщение, разделяемая `send_telegram` не тронута. Динамические подписи экранируются `html.escape`, `parse_mode=HTML` сохранён. ADR `docs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md`. Тесты: `test_notify_approve_links.py`, `test_analysis_approve_flow_links.py`.
- **Конфигурируемые модель LLM и режим работы (`--effort`) агентов** (ORCH-41): модель/effort каждого агента вынесены из хардкода `launcher.py` в конфиг — глобально per-agent (`ORCH_AGENT_MODEL_<AGENT>` / `ORCH_AGENT_EFFORT_<AGENT>`, дефолты `ORCH_AGENT_MODEL_DEFAULT=claude-opus-4-8`, `ORCH_AGENT_EFFORT_DEFAULT=high`) и per-project (`agent_models` / `agent_efforts` в `ORCH_PROJECTS_JSON`). Резолверы `resolve_agent_model` / `resolve_agent_effort` (приоритет project > per-agent env > default > пусто), валидация effort `{low,medium,high,xhigh,max}`, опц. `ORCH_AGENT_FALLBACK_MODEL` (`--fallback-model`). Хардкод `"model":"opus"` (architect/reviewer) удалён. Тесты: `test_resolve_agent_model.py`, `test_resolve_agent_effort.py`.
- **Единый status-коммент агентов в Plane** (ORCH-016): `usage.build_status_comment(...)` — один хелпер для ВСЕХ ролей (analyst..deployer). HTML-формат: header `{icon} {Role} — {описание}`, опциональная строка `Verdict/Status: …` из YAML-frontmatter артефакта, **строка `Длительность: 4m 12s`** (явный `duration_s` от launcher, fallback из `agent_runs` для аналитика), `<b>Документы:</b><ul><li><a>…</a></li></ul>`, тех-хвост `<sub>tokens · cost</sub>`. Утилитки: `usage.fmt_duration`, `usage.get_agent_duration`, новый модуль `src/frontmatter.py` (defensive YAML reader). ADR `docs/work-items/ORCH-016/06-adr/ADR-001-unified-status-comment.md`.
@@ -21,6 +22,7 @@
- Цепочка стадий: `... testing → deploy-staging → deploy → done` (была без `deploy-staging`).
### Fixed
- **Testing-гейт `check_tests_passed` читает `result:` наравне с `verdict:`/`status:`** (ORCH-047): парсер `_parse_tests_verdict` (`src/qg/checks.py`) теперь принимает три равноправных машиночитаемых поля frontmatter `13-test-report.md``result:` (канон промпта тестера `.openclaw/agents/tester.md`, `result: PASS|FAIL`), плюс легаси `verdict:` и `status:` (enduro-trails ET-001..ET-014); достаточно любого одного непустого. Устраняет рассинхрон контракта: тестер честно эмитил `result: PASS` без `verdict:`/`status:`, парсер попадал в ветку «нет машинного вердикта» → откат `testing → development` в петлю до исчерпания `MAX_DEVELOPER_RETRIES` (наблюдалось на ORCH-17; ORCH-016 прошёл лишь из-за избыточного дублирования полей). Семантика приоритетов сохранена и распространена на все три поля через объединённую строку: negative-токен в любом поле авторитетен (перебивает positive), наборы токенов заморожены (обратная совместимость). Сигнатура гейта, имя и реестр `QG_CHECKS` не менялись. ADR `docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md`. Тесты: `tests/test_qg.py::TestCheckTestsPassed`.
- БАГ-8: провал deploy/deploy-staging → корректный откат на `development`.
- Изоляция тестов от живого Plane API (PR #27): autouse-фикстура сброса settings.

View File

@@ -58,7 +58,7 @@ created → analysis → architecture → development → review → testing →
```
- **Длительность** считается launcher'ом (`_monitor_agent`) и пробрасывается в `_post_usage_comments`; для analyst (коммент строится в `stage_engine`) используется DB-фоллбэк `usage.get_agent_duration(task_id, agent)`.
- **Vердикт-парсер** — `src/frontmatter.read_frontmatter_value(...)` (defensive, никогда не raise). Машинные ключи: `verdict:` (reviewer/tester), `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md).
- **Vердикт-парсер** — `src/frontmatter.read_frontmatter_value(...)` (defensive, никогда не raise). Машинные ключи: reviewer → `verdict:` (12-review.md); **testing-гейт `check_tests_passed` (13-test-report.md) → любое из трёх равноправных: `result:` (канон промпта тестера), `verdict:`, `status:`** (ORCH-047, ADR-001); deployer → `deploy_status:` (14-deploy-log.md), `staging_status:` (15-staging-log.md). Negative-токен в любом поле авторитетен (перебивает positive).
- Формат коммента **не** меняет реестр гейтов и стадий; коммент — отображение, не управление.
## База данных (SQLite)

View File

@@ -8,6 +8,7 @@ Per-work-item решения живут в `docs/work-items/<id>/06-adr/ADR-NNN-
| adr-0001 | Реестр проектов (multi-repo) | accepted | 2026-06-02 | ORCH-6 |
| adr-0002 | Очередь задач вместо in-process потоков | accepted | 2026-06-03 | ORCH-1 |
| adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 |
| adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 |
## Формат
**Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded.

View File

@@ -0,0 +1,45 @@
# adr-0004: Поллинг с ретраем в quality-gate check_ci_green (фикс CI-race)
- **Статус:** accepted
- **Дата:** 2026-06-05
- **Задача:** ORCH-045
## Контекст
Quality-gate `check_ci_green(repo, branch)` (`src/qg/checks.py`) проверяет combined commit-status ветки через Gitea API сразу после того, как developer-агент запушил код. Реализация была **single-shot**: один `GET /repos/{owner}/{repo}/commits/{branch}/status`, чтение `data["state"]``success` → пропуск, иначе → сразу `False`.
Это создавало race condition. Gitea-CI после пуша 13 секунды держит combined state `pending`, пока не отработают чек-раннеры. Если гейт опрашивал статус в этом окне, он получал `pending` и возвращал `False` **ровно один раз** — повторного опроса не было. Combined state затем дозеленевал до `success`, но гейт уже промахнулся, и задача застревала насмерть без видимой причины.
Реальный инцидент **ORCH-017**: гейт опросил статус в 17:58:54 → `pending`; CI дозеленел в 17:58:55. Задача встала в тупик (см. `docs/history` / lessons ORCH-017).
## Решение
`check_ci_green` превращён из single-shot в **polling с ретраем**:
- `state == "success"``(True, "CI green")` немедленно.
- `state in ("failure", "error")``(False, "CI state: <state>")` немедленно — CI красный, ретрай бессмыслен (терминальное состояние).
- `state == "pending"` (или `unknown` / иное не-терминальное) → `time.sleep(interval)` и опрос снова, до `N` попыток.
- После исчерпания всех попыток при всё ещё `pending``(False, "CI still pending after <T>s")`**явный** провал с причиной, чтобы оператор видел тупик, а не молчаливый стол.
- `404``(False, "Branch ... not found or no status")` — как раньше.
- Транзиентная `httpx.HTTPError` на отдельной попытке — **не падаем сразу**: логируем и пробуем ещё в рамках лимита попыток; если все попытки — сетевая ошибка → `(False, "API error: <e>")`.
Параметры вынесены в `src/config.py` (pydantic-settings, env-prefix `ORCH_`, единый стиль с остальными настройками):
- `ci_poll_max_attempts` (env `ORCH_CI_POLL_MAX_ATTEMPTS`, дефолт **12**)
- `ci_poll_interval_s` (env `ORCH_CI_POLL_INTERVAL_S`, дефолт **10**)
Итого по умолчанию гейт ждёт `pending` до ~2 минут (12 × 10s) перед тем как явно провалиться. Каждая не-финальная попытка логируется через существующий `logger` (`check_ci_green: attempt i/N, state=..., retrying in Ns`). `timeout=10` на каждый отдельный запрос сохранён.
Сигнатура `check_ci_green(repo, branch) -> tuple[bool, str]` **не менялась** — её зовёт stage_engine и реестр гейтов `QG_CHECKS`.
## Альтернативы
- **Оставить single-shot, опрашивать гейт повторно снаружи (на уровне stage_engine/воркера).** Отклонено: размазывает логику CI-ожидания по слоям, дублирует таймауты; гейт — естественное место знания о combined-status.
- **Webhook от Gitea на завершение CI вместо поллинга.** Отложено: требует надёжной доставки/дедупликации вебхуков именно по CI-статусу и переписывания триггера стадии; поллинг — минимальный, локализованный фикс race-а здесь и сейчас.
- **Бесконечный ретрай до зелёного.** Отклонено: задача могла бы висеть вечно при реально зависшем CI; ограниченный бюджет + явный `False` с причиной даёт оператору сигнал.
## Последствия
- CI-race ORCH-017 закрыт: транзиентный `pending` переживается ретраем, гейт не промахивается.
- `check_ci_green` теперь **блокирующий** до ~`max_attempts × interval` секунд при затяжном `pending` (по умолчанию ~2 мин). Это осознанный trade-off; для красного CI и success — выход немедленный, без задержки.
- Тупик больше не молчаливый: истечение попыток → `(False, "CI still pending after <T>s")`, причина видна.
- Бюджет/интервал настраиваемы через env без правки кода.
- `check_tests_passed` / `_parse_tests_verdict` (ORCH-47) **не затронуты**.
## Связи
ORCH-017 (инцидент-первоисточник: deadlock shared-gate из-за CI-race), реестр гейтов `QG_CHECKS` (`check_ci_green`), стадия `development`. Тесты: `tests/test_qg.py::TestCheckCIGreen`.

View File

@@ -0,0 +1,128 @@
# Lessons Learned — 2026-06-05 (вечер): ORCH-17/45/47 + деплой прода
## Итог дня
Закрыты три задачи (ORCH-17, ORCH-45, ORCH-47), два прод-гейта стали умнее, заведено
4 системных задачи в бэклог (ORCH-44/46/48 + B6). Главный сквозной урок: **конвейер не мог
провести эти задачи автономно из-за дыр в самом конвейере** — потребовались ручные merge и
ребилды прода. Корни задокументированы, чинятся отдельными задачами.
---
## 1. ORCH-17 — approve-ping links (закрыта вручную)
Подробный разбор: `docs/history/LESSONS_ORCH-017.md`. Кратко: косметика (2 ссылки)
застряла 5 раз, объективный дедлок shared-гейта, ручной merge PR #37 (`26c6f267`).
---
## 2. ORCH-45 — CI-гонка в `check_ci_green` (исправлена, в проде)
### Проблема
`check_ci_green` делал **один** запрос статуса CI сразу после developer. Если CI ещё
`pending` 1-3 секунды (реальный кейс: опрос 17:58:54 → pending, CI позеленел 17:58:55) —
гейт возвращал False **один раз** и задача застревала насмерть с зелёным CI.
### Решение (PR #39, merge `982698c4`)
Поллинг с ретраем: `success`/`failure` — терминальны (сразу), `pending` → ждать
`CI_POLL_INTERVAL_S`(10с) до `CI_POLL_MAX_ATTEMPTS`(12) раз, истёк лимит → явный
`False` с причиной "CI still pending after Ns" (не виснет молча). Параметры в `config.py`
как env `ORCH_CI_POLL_*`. ADR-0004. +5 тестов (мок httpx + time.sleep).
---
## 3. ORCH-47 — тестер-гейт игнорил `result:` (исправлен, в проде)
### Проблема (уловка-22)
`check_tests_passed`/`_parse_tests_verdict` читал только `verdict:`/`status:` из frontmatter
`13-test-report.md`, но промпт tester-агента велит писать `result: PASS|FAIL`. Честный тестер
(`result: PASS`, без `verdict:`) → гейт «No machine-readable verdict» → ложный FAIL → петля
dev↔review↔tester → Blocked. **И сама ORCH-47 (которая это чинит) попала в тот же капкан:**
в проде крутился старый гейт → не понимал её собственный `result: PASS` → 3 круга петли.
Змея кусает хвост: чтобы пройти гейт автономно, фикс уже должен быть в проде.
### Решение (PR #40, merge `5d04de9e`)
`result:` добавлен как равноправное поле наряду с `verdict:`/`status:`. Любое одно непустое
поле достаточно. Negative-токен (BLOCKED/FAILED) в ЛЮБОМ поле авторитетен (ET-013 кейс
сохранён). Token sets заморожены для обратной совместимости. ADR-001. +6 тестов (68 passed).
После деплоя ручной `advance_stage` пнул застрявшую task → гейт принял `result: PASS`
прошёл testing. Петля исчезла навсегда.
### Остаточная находка → B6 / ORCH-48
На staging деплоер дал 9/10 PASS, завалил **B6 Registry isolation**: staging-реестр видит
боевые ET+ORCH вместо одного sandbox (нарушает «staging — только sandbox»). Деплоер честно
поставил FAILED и НЕ стал натягивать зелёнку (вне мандата) → откат by design. К фиксу гейта
отношения не имеет (E2E против sandbox прошёл). Заведена ORCH-48.
---
## 4. ДЕПЛОЙ ПРОДА — как правильно (важная операционная памятка)
### `/app` запечён в образ, НЕ volume
`docker-compose.yml`: `build: .` + `COPY src/ ./src/`. Поэтому `git pull` + рестарт с
`--no-build` **НЕ довозит код** — нужен `docker compose build orchestrator`. Деплой-хук
(`scripts/orchestrator-deploy-hook.sh`) по дефолту целит в **staging** (by design) — для
прода нужны env `TARGET_SERVICE=orchestrator TARGET_PORT=8500 COMPOSE_PROFILE=''`.
### Порты/профили
- prod orchestrator = порт **8500** (`/health``{"status":"ok"}`), `network_mode: host`,
профиль prod = пустой (стартует обычным `docker compose up -d orchestrator`).
- staging = порт **8501**, профиль `staging` (стартует только `--profile staging`).
### Рабочая последовательность деплоя (проверена дважды 05.06)
1. `sudo chown -R slin:slin /home/slin/repos/orchestrator` (см. грабля ниже).
2. `git checkout main && git reset --hard origin/main && git clean -fd -e '*.bak*' -e '.deploy-prev-image-prod'`.
3. `docker compose build orchestrator`.
4. `docker compose up -d orchestrator` + health-loop на :8500.
5. **Проверка claude-auth** (ребилд её ломает — см. ниже).
6. Проверка что новый код активен в `/app` (grep маркера правки).
### ⚠️ ГРАБЛЯ: хост-репо рассинхронизирован с git (агенты пишут под root)
Хост-репо `/home/slin/repos/orchestrator` оказывался на feature-ветке (не main), а рабочая
копия засеяна untracked+modified файлами, созданными агентами **под uid=0 (root-owned)** прямо
в репо. → `git pull --ff-only` падал `Permission denied` / `would be overwritten`, обычный
`rm` под slin не мог снести root-файлы. **Лечение:** `sudo chown -R slin:slin <repo>`
проверить что modified=совпадает-с-main и untracked=уже-в-main (дубликаты, не теряем) →
`git reset --hard origin/main` + `git clean`. **Хук это НЕ разруливает** — сверять состояние
хост-репо перед каждым деплоем.
### ⚠️ ГРАБЛЯ: ребилд ломает claude-auth (проверять ВСЕГДА)
Пересоздание контейнера может root-овнить `/home/slin/.claude/.credentials.json` и сделать
`/root/.claude` пустышкой → агенты падают `Not logged in`. Защита — монтирование creds в
compose (`/home/slin/.claude` + `.claude.json`), launcher форсит `HOME=/home/slin`.
**После каждого ребилда боевая проверка:**
`docker exec orchestrator bash -c 'cd /tmp && HOME=/home/slin /opt/claude-code/bin/claude.exe --print "ОК"'`
(timeout 90с). 05.06 auth пережил оба ребилда — защита держит.
---
## 5. ЗАПУСК конвейера и Gitea API
### Старт конвейера = Plane Backlog → In Progress
Конвейер стартует штатно переводом задачи в Plane из Backlog в **In Progress** (код:
`webhooks/plane.py handle_status_start` — «pipeline is started when Slava moves the issue
into In Progress»). Webhook создаёт task-row, заводит ветку, запускает analyst. Никаких
ручных вставок в БД.
### QG-0: лимит заголовка 80 символов
При старте задача с заголовком >80 символов заворачивается на QG-0 («Title слишком длинный»)
и уходит в Blocked. Чинить — укоротить `name` (суть в заголовок, детали в description),
вернуть в Backlog, снова In Progress.
### Gitea API грабли
- **merge/create PR** требуют заголовок `Authorization: token <ORCH_GITEA_TOKEN>` (форма
с префиксом `token `), иначе 401 "token is required".
- **heredoc через ssh+docker exec глотает вывод** python-скрипта. Надёжный путь: написать
`.py` локально → `base64 -w0``ssh "echo <b64> | base64 -d > /tmp/x.py"``docker cp`
`docker exec python3 /tmp/x.py`. Это же обходит экранирование кириллицы/скобок.
---
## Состояние прод-гейтов после 05.06
-`check_ci_green` — поллинг с ретраем (ORCH-45)
-`check_tests_passed` — читает `result:`/`verdict:`/`status:` (ORCH-47)
## Бэклог (high) после дня
- **ORCH-44** — надёжность запуска агента (preflight слеп к auth; `--effort` гасит вывод;
пустой run-лог → должен быть failed).
- **ORCH-46** — «испорченный телефон»: орк не передаёт деву ТЕКСТ замечаний reviewer/tester
(только ссылку на файл), противоречивые сигналы tester↔reviewer, нет памяти между кругами.
- **ORCH-48 / B6** — staging registry isolation (staging видит прод-проекты вместо sandbox).

View File

@@ -0,0 +1,7 @@
# Business Request: check_tests_passed: gate must read result: field from test report
Work Item ID: ORCH-047
## Description
TBD

View File

@@ -0,0 +1,57 @@
# BRD — ORCH-047: check_tests_passed должен читать поле `result:` из тест-отчёта
## 1. Контекст и проблема
Quality Gate `check_tests_passed` (`src/qg/checks.py`, функция-парсер `_parse_tests_verdict`) гейтит переход `testing → deploy-staging`. Он читает машиночитаемый вердикт из YAML-frontmatter артефакта `13-test-report.md`.
**Дефект (обнаружен дев-агентом в ходе ORCH-17, подтверждён 05.06.2026):**
парсер читает ТОЛЬКО поля `verdict:` и `status:`. Однако промпт тестер-агента (`.openclaw/agents/tester.md`, строки 5156 и 7880) предписывает эмитить машиночитаемое поле **`result: PASS|FAIL`** — и НЕ упоминает ни `verdict:`, ни `status:`.
В результате тестер, честно следующий своей инструкции (реальный отчёт ORCH-017: `result: PASS`, без `verdict:`/`status:`), упирается в ветку «ни verdict, ни status не заданы» → гейт возвращает `False` с причиной *"No machine-readable verdict/status in test report frontmatter"* → задача откатывается `testing → development`.
**Последствие:** ЛЮБАЯ задача, где тестер пишет `result: PASS` (то есть строго по своей инструкции), застревает в бесконечной петле `testing ↔ development` до исчерпания `MAX_DEVELOPER_RETRIES`. Именно это крутило ORCH-17. ORCH-016 прошёл раньше лишь потому, что его отчёт избыточно нёс И `verdict:`, И `result:`.
**Корень:** рассинхрон контракта. Гейт (потребитель) и промпт тестера (производитель) описывают разные имена машиночитаемого поля.
## 2. Бизнес-цель
Привести контракт гейта `check_tests_passed` в соответствие с тем, что тестер-агенту реально велено эмитить, чтобы корректные тест-отчёты (`result: PASS`) проходили гейт, а отрицательные (`result: FAIL`) — надёжно откатывали задачу. Устранить ложноотрицательные срабатывания, ломающие конвейер всех проектов.
## 3. Заинтересованные стороны
- **Owner / Стрим, Слава** — выявили дефект при разборе ORCH-17 (05.06).
- **Все проекты общего прод-инстанса** (orchestrator self-hosting + enduro-trails) — потребители shared quality-gate. Это SHARED-изменение, влияет на всех.
- **Тестер-агент** — производитель `13-test-report.md`.
## 4. Объём работ (scope)
### В объёме
- `_parse_tests_verdict` читает `result:` как первоклассное машиночитаемое поле НАРАВНЕ с `verdict:` и `status:`.
- Семантика приоритетов сохраняется и распространяется на все три поля:
- negative-токен в ЛЮБОМ из трёх (`result`/`verdict`/`status`) → FAIL и авторитетен (перебивает positive в другом поле);
- при отсутствии negative — positive-токен в ЛЮБОМ из трёх → PASS;
- ни одно из трёх полей не задано → FAIL (нет машиночитаемого вердикта);
- заданы, но не распознаны → FAIL.
- Обратная совместимость: отчёты, несущие только `verdict:`/`status:` (стиль enduro-trails ET-001…ET-014, ORCH-016), продолжают работать ровно как раньше.
- **ADR** на изменение семантики shared testing-гейта (правило 2 CLAUDE.md — обязательно для сквозного изменения).
- Обновление документации: `docs/architecture/README.md` (строка про машинные ключи вердикт-парсера), `CHANGELOG.md`.
### Вне объёма
- Изменение промпта тестера (`.openclaw/agents/tester.md`). Контракт приводится со стороны гейта к тому, что тестеру УЖЕ велено эмитить; промпт не трогаем.
- Изменение других гейтов (`check_reviewer_verdict`, `check_deploy_status`, `check_staging_status`) — у них свои поля (`verdict:`, `deploy_status:`, `staging_status:`), они вне этого дефекта.
- Изменения ORCH-017 (про ссылки) — это отдельный work item.
## 5. Ограничения и риски
- **SHARED quality-gate, общий прод-инстанс.** Изменение затрагивает enduro-trails наравне с orchestrator. Регресс недопустим: набор положительных/отрицательных токенов и поведение для старого формата (`verdict:`/`status:`) должны остаться неизменными.
- **Self-hosting.** Орк правит сам себя; деплой проходит через обязательную стадию `deploy-staging` (8501). Прод-контейнер `orchestrator` (8500) не ронять.
- Изменение читает только frontmatter, никогда не прозу (канон гейтов из `docs/architecture/README.md`).
- Парсер не должен бросать исключения ни при каком вводе (битый YAML, пустой файл, frontmatter-не-mapping) → всегда `(False, reason)`.
## 6. Эталонный код
Дев-агент уже написал референс-реализацию в ветке `feature/ORCH-017` (`src/qg/checks.py` + `tests/test_qg.py`, 23 теста). Его допустимо использовать как ориентир, но оформить чисто через данный work item с собственным ADR.
## 7. Критерий успеха
Тест-отчёт с одним лишь `result: PASS` проходит гейт `check_tests_passed`; с `result: FAIL` — нет. Старый формат (`verdict:`/`status:`) не регрессирует. Все pytest зелёные. ADR заведён.

View File

@@ -0,0 +1,68 @@
# ТЗ — ORCH-047: `_parse_tests_verdict` читает `result:` наравне с `verdict:`/`status:`
## 1. Задействованные модули `src/`
| Файл | Что меняется |
|------|--------------|
| `src/qg/checks.py` | Функция `_parse_tests_verdict` (стр. ~223265). Добавить чтение поля `result:` из frontmatter и включить его в проверку токенов наравне с `verdict:`/`status:`. Обновить докстринг функции и `check_tests_passed`. |
Точка входа `check_tests_passed(repo, work_item_id, branch)` (стр. ~182) и реестр `QG_CHECKS` НЕ меняются (сигнатура и имя гейта те же).
## 2. Требуемое поведение `_parse_tests_verdict`
Вход — строковое тело `13-test-report.md`. Выход — `tuple[bool, str]`.
1. Нет frontmatter (`content` не начинается с `---`) → `(False, "No YAML frontmatter ...")`.
2. Frontmatter некорректен (split по `---` даёт < 3 частей) → `(False, "Malformed YAML frontmatter ...")`.
3. YAML не парсится → `(False, "Invalid YAML frontmatter ...: <e>")` (никогда не raise).
4. YAML не mapping → `(False, "Malformed YAML frontmatter ... (not a mapping)")`.
5. Прочитать три поля, нормализовать (`str(...).upper().strip()`, защита от `None`):
- `verdict`
- `status`
- **`result`НОВОЕ**
6. Если ВСЕ три пусты → `(False, "No machine-readable verdict/status/result in test report frontmatter")`.
7. Собрать объединённую строку полей `fields = f"{verdict} {status} {result}"`.
8. Если в `fields` встречается ЛЮБОЙ negative-токен (`_TESTS_NEGATIVE_TOKENS`) → `(False, "Test verdict: <значение> (<NEG>)")`. **Negative авторитетен** — проверяется ПЕРВЫМ, перебивает любой positive.
9. Иначе если встречается ЛЮБОЙ positive-токен (`_TESTS_POSITIVE_TOKENS`) → `(True, "Test verdict: <значение> (PASS)")`.
10. Иначе (заданы, но не распознаны) → `(False, "No recognized PASS verdict in frontmatter (...)")`.
Наборы токенов НЕ изменяются (важно для обратной совместимости с enduro-trails):
```python
_TESTS_NEGATIVE_TOKENS = ("BLOCKED", "FAILED", "FAIL", "REQUEST_CHANGES", "REJECT", "RED")
_TESTS_POSITIVE_TOKENS = ("PASSED", "PASS", "READY-TO-DEPLOY", "READY_TO_DEPLOY", "GREEN", "APPROVED")
```
> Примечание для разработчика (порядок токенов): negative-список проверяется раньше positive — это даёт авторитетность отрицания. Внутри positive-набора `"PASSED"` идёт перед `"PASS"` лишь для аккуратного reason-текста; на результат (bool) порядок не влияет, т.к. это подстрочный поиск.
## 3. Контракт поля (golden source)
После изменения машиночитаемыми полями testing-гейта считаются **три равноправных**: `result:` (канон промпта тестера), `verdict:`, `status:` (легаси/enduro-trails). Достаточно ЛЮБОГО одного. Это и есть приведение гейта к тому, что тестеру велено эмитить в `.openclaw/agents/tester.md` (`result: PASS|FAIL`).
## 4. Изменения API
Нет. HTTP-эндпоинты (`/health`, `/status`, `/queue`, вебхуки) не затрагиваются. Сигнатуры функций гейта не меняются.
## 5. Изменения схемы БД
Нет.
## 6. Требования к новым QG checks
Новых гейтов нет. Меняется внутренняя логика существующего `check_tests_passed` (через `_parse_tests_verdict`). Реестр `QG_CHECKS` без изменений → снапшот-тест `tests/test_qg_registry_snapshot.py` должен остаться зелёным.
## 7. Артефакты pipeline (создать/обновить в этом PR)
- `docs/work-items/ORCH-047/06-adr/ADR-001-*.md`**обязательно** (правило 2 CLAUDE.md): ADR на изменение семантики SHARED testing-гейта (влияет на все проекты общего инстанса). Заводит архитектор.
- `docs/architecture/README.md` — обновить строку о вердикт-парсере (раздел «Plane Sync», п. про машинные ключи): для testing-гейта перечислить `result:`/`verdict:`/`status:`.
- `CHANGELOG.md` — запись `fix:` про ORCH-047.
- `tests/test_qg.py` — добавить кейсы на `result:` (см. `04-test-plan.yaml`).
## 8. Нефункциональные требования
- Парсер не бросает исключений ни на каком вводе.
- Изменение читает только frontmatter, не прозу (канон гейтов).
- Полная обратная совместимость: существующие тесты `TestCheckTestsPassed` остаются зелёными без правок (кроме, возможно, переименования reason-строки в п.6 BRD — текст причины «No machine-readable verdict/status...» обновляется на «...verdict/status/result...», соответствующий ассерт при наличии обновить).
## 9. Деплой
Self-hosting: стандартный путь через `deploy-staging` (8501) перед прод-деплоем. Прод-контейнер `orchestrator` (8500) не перезапускать в рамках разработки/тестинга.

View File

@@ -0,0 +1,68 @@
# Критерии приёмки — ORCH-047
Каждый критерий имеет однозначное условие PASS/FAIL.
## AC-01 — `result: PASS` проходит гейт (главный кейс ORCH-17)
- **Дано:** `13-test-report.md` с frontmatter, содержащим только `result: PASS` (без `verdict:`/`status:`).
- **Ожидается:** `check_tests_passed(...)``(True, ...)`, в reason присутствует «PASS».
- **PASS:** возвращается True. **FAIL:** возвращается False.
## AC-02 — `result: FAIL` откатывает задачу
- **Дано:** frontmatter с `result: FAIL` (без `verdict:`/`status:`).
- **Ожидается:** `(False, ...)`, reason содержит токен отрицания (`FAIL`).
- **PASS:** False. **FAIL:** True.
## AC-03 — Negative авторитетен поверх positive (в т.ч. между полями)
- **Дано:** `result: PASS`, но `verdict: BLOCKED` (или `status: failed`).
- **Ожидается:** `(False, ...)`, reason упоминает negative-токен (`BLOCKED`/`FAILED`).
- **PASS:** False. **FAIL:** True.
## AC-04 — Positive в любом из трёх полей даёт PASS
- **Дано (каждый подкейс отдельно):**
- только `verdict: PASS`;
- только `status: PASSED`;
- только `result: ready-to-deploy`.
- **Ожидается:** все три → `(True, ...)`.
- **PASS:** все True. **FAIL:** хоть один False.
## AC-05 — Обратная совместимость (enduro-trails / ORCH-016)
- **Дано:** существующие реальные формы из `TestCheckTestsPassed`:
- `verdict: PASS` + `status: pass`;
- `verdict: PASS — ready-to-deploy`;
- `verdict: ready-to-deploy` + `status: PASSED`;
- `verdict: stage:ready-to-deploy` + `status: pass`;
- `verdict: BLOCKED` + проза «23 passed».
- **Ожидается:** результаты идентичны прежним (PASS-кейсы → True, BLOCKED → False). Старые тесты `TestCheckTestsPassed` зелёные.
- **PASS:** поведение не изменилось. **FAIL:** любой регресс.
## AC-06 — Ни одно из трёх полей не задано → FAIL
- **Дано:** frontmatter без `result`/`verdict`/`status` (например, только `type:`/`version:`); тело может содержать «Result: PASS» прозой.
- **Ожидается:** `(False, ...)`, причина про отсутствие машиночитаемого вердикта.
- **PASS:** False. **FAIL:** True.
## AC-07 — Только проза, без frontmatter → FAIL
- **Дано:** отчёт без YAML-frontmatter, в теле «Result: PASS / All tests passed».
- **Ожидается:** `(False, ...)`, причина про отсутствие frontmatter. Прозу не читаем.
- **PASS:** False. **FAIL:** True.
## AC-08 — Битый YAML → FAIL без исключения
- **Дано:** некорректный YAML во frontmatter.
- **Ожидается:** `(False, ...)` c упоминанием YAML/frontmatter, функция НЕ бросает исключение.
- **PASS:** False и нет raise. **FAIL:** raise или True.
## AC-09 — Отчёт отсутствует → FAIL
- **Дано:** файла `13-test-report.md` нет.
- **Ожидается:** `(False, "...not found...")`.
- **PASS:** False. **FAIL:** True.
## AC-10 — Реестр гейтов неизменен
- **Ожидается:** `QG_CHECKS` содержит ровно те же ключи, что и до изменения; `tests/test_qg_registry_snapshot.py` зелёный.
- **PASS:** снапшот совпал. **FAIL:** снапшот изменился.
## AC-11 — Документация и ADR обновлены (правило 2/6 CLAUDE.md)
- **Ожидается:** заведён `docs/work-items/ORCH-047/06-adr/ADR-001-*.md`; обновлены `docs/architecture/README.md` (вердикт-парсер testing-гейта) и `CHANGELOG.md`.
- **PASS:** все три присутствуют и описывают изменение. **FAIL:** что-либо отсутствует → REQUEST_CHANGES на review.
## AC-12 — Полный регресс зелёный
- **Ожидается:** `pytest tests/ -q` — все тесты PASS.
- **PASS:** exit code 0. **FAIL:** любой упавший тест.

View File

@@ -0,0 +1,97 @@
work_item: ORCH-047
module_under_test: src/qg/checks.py::_parse_tests_verdict (via check_tests_passed)
test_file: tests/test_qg.py
notes: >
Добавить в класс TestCheckTestsPassed. Шаблон записи отчёта — существующий
хелпер self._write(dir, content). Наборы токенов не меняются; проверяем, что
поле result: теперь равноправно с verdict:/status:, а старые кейсы не регрессируют.
tests:
- id: TC-01
type: unit
description: "result: PASS без verdict/status -> гейт PASS (главный кейс ORCH-17, AC-01)"
module: tests/test_qg.py
fixture_frontmatter: "---\ntype: test-report\nresult: PASS\n---\n\n# Test Report\n"
expected: PASS
- id: TC-02
type: unit
description: "result: FAIL без verdict/status -> гейт FAIL, reason содержит FAIL (AC-02)"
module: tests/test_qg.py
fixture_frontmatter: "---\nresult: FAIL\n---\n\nbody\n"
expected: FAIL
- id: TC-03
type: unit
description: "result: PASS, но verdict: BLOCKED -> negative авторитетен -> FAIL (AC-03)"
module: tests/test_qg.py
fixture_frontmatter: "---\nresult: PASS\nverdict: BLOCKED\n---\n\n23 passed\n"
expected: FAIL
- id: TC-04
type: unit
description: "result: PASS, но status: failed -> negative авторитетен -> FAIL (AC-03)"
module: tests/test_qg.py
fixture_frontmatter: "---\nresult: PASS\nstatus: failed\n---\n\nbody\n"
expected: FAIL
- id: TC-05
type: unit
description: "result: ready-to-deploy (positive-токен, без слова PASS) -> PASS (AC-04)"
module: tests/test_qg.py
fixture_frontmatter: "---\nresult: ready-to-deploy\n---\n\nbody\n"
expected: PASS
- id: TC-06
type: unit
description: "Только verdict: PASS (легаси) -> PASS, без регресса (AC-05)"
module: tests/test_qg.py
fixture_frontmatter: "---\nverdict: PASS\nstatus: pass\n---\n\nbody\n"
expected: PASS
- id: TC-07
type: unit
description: "verdict: BLOCKED + проза '23 passed' (ET-013 баг) -> FAIL, без регресса (AC-05)"
module: tests/test_qg.py
fixture_frontmatter: "---\nverdict: BLOCKED\n---\n\nTests: 23 passed, 0 failed.\n"
expected: FAIL
- id: TC-08
type: unit
description: "Ни result, ни verdict, ни status; тело с прозой 'Result: PASS' -> FAIL (AC-06)"
module: tests/test_qg.py
fixture_frontmatter: "---\ntype: test-report\nversion: 1\n---\n\nResult: PASS\n"
expected: FAIL
- id: TC-09
type: unit
description: "Нет frontmatter, проза 'Result: PASS' -> FAIL (AC-07)"
module: tests/test_qg.py
fixture_frontmatter: "# Test Report\n\nResult: PASS\nAll tests passed.\n"
expected: FAIL
- id: TC-10
type: unit
description: "Битый YAML во frontmatter -> FAIL без исключения, reason про YAML/frontmatter (AC-08)"
module: tests/test_qg.py
fixture_frontmatter: "---\nresult: [unclosed\n : : :\n---\n\nbody PASS\n"
expected: FAIL
- id: TC-11
type: unit
description: "Файл 13-test-report.md отсутствует -> FAIL, reason 'not found' (AC-09)"
module: tests/test_qg.py
fixture_frontmatter: null
expected: FAIL
- id: TC-12
type: unit
description: "Реестр QG_CHECKS не изменился -> снапшот зелёный (AC-10)"
module: tests/test_qg_registry_snapshot.py
expected: PASS
- id: TC-13
type: integration
description: "Полный регресс pytest tests/ -q зелёный, существующий TestCheckTestsPassed без правок логики (AC-05, AC-12)"
module: tests/
expected: PASS

View File

@@ -0,0 +1,80 @@
# ADR-001: testing-гейт читает `result:` наравне с `verdict:`/`status:`
- **Статус:** Accepted
- **Дата:** 2026-06-05
- **Задача:** ORCH-047
- **Область:** SHARED quality-gate `check_tests_passed` (общий прод-инстанс: orchestrator + enduro-trails)
## Контекст
Quality Gate `check_tests_passed` (`src/qg/checks.py`, парсер `_parse_tests_verdict`) гейтит
переход `testing → deploy-staging`, читая машиночитаемый вердикт ТОЛЬКО из YAML-frontmatter
артефакта `13-test-report.md` (канон гейтов: frontmatter, никогда не проза — см.
`docs/architecture/README.md`).
Существует рассинхрон контракта между производителем и потребителем вердикта:
- **Потребитель** (`_parse_tests_verdict`) читает поля `verdict:` и `status:`.
- **Производитель** (`.openclaw/agents/tester.md`, строки 5156, 7880) предписывает тестеру
эмитить машиночитаемое поле **`result: PASS|FAIL`** и НЕ упоминает `verdict:`/`status:`.
Тестер, честно следуя своей инструкции, пишет `result: PASS` без `verdict:`/`status:`. Парсер
попадает в ветку «ни verdict, ни status не заданы» → `(False, "No machine-readable
verdict/status…")` → откат `testing → development` и петля до исчерпания
`MAX_DEVELOPER_RETRIES`. Это наблюдалось на ORCH-17; ORCH-016 прошёл лишь потому, что его отчёт
избыточно нёс И `verdict:`, И `result:`.
Корень — несовпадение имён поля контракта, а не логики токенов. Наборы positive/negative-токенов
исправны и менять их нельзя (обратная совместимость с реальными отчётами enduro-trails
ET-001…ET-014).
## Решение
Привести контракт гейта к тому, что тестеру УЖЕ велено эмитить — со стороны гейта, не трогая
промпт тестера.
1. `_parse_tests_verdict` читает **три равноправных** машиночитаемых поля из frontmatter:
`result:` (канон промпта тестера), `verdict:`, `status:` (легаси/enduro-trails). Достаточно
ЛЮБОГО одного непустого.
2. Семантика приоритетов сохраняется и распространяется на все три поля через объединённую строку
`fields = f"{verdict} {status} {result}"`:
- negative-токен (`_TESTS_NEGATIVE_TOKENS`) в любом поле → FAIL и **авторитетен** (проверяется
первым, перебивает positive в другом поле);
- иначе positive-токен (`_TESTS_POSITIVE_TOKENS`) в любом поле → PASS;
- ни одно из трёх не задано → FAIL («No machine-readable verdict/status/result…»);
- заданы, но не распознаны → FAIL.
3. Наборы токенов **не изменяются**.
4. Парсер не бросает исключений ни на каком вводе (битый YAML, пустой файл, frontmatter-не-mapping)
→ всегда `(False, reason)`.
5. Сигнатура `check_tests_passed`, имя гейта и реестр `QG_CHECKS` **не меняются** — снапшот
`tests/test_qg_registry_snapshot.py` остаётся зелёным.
### Альтернативы (отклонены)
- **Править промпт тестера** (`verdict:` вместо `result:`) — отклонено: контракт уже задокументирован
для тестера как `result:`; единичная правка гейта дешевле и не требует переучивать агента, плюс
ломала бы совместимость со старыми отчётами, где встречается `verdict:`/`status:`.
- **Глобальный ADR в `docs/architecture/adr/`** — не требуется: изменение не добавляет гейт/стадию/
компонент и не меняет топологию; это приведение парсинга существующего гейта к контракту. Канон
гейтов в README обновляется точечно.
## Последствия
- **Плюс:** корректные отчёты `result: PASS` проходят гейт; `result: FAIL` надёжно откатывает.
Петля `testing ↔ development` устранена для всех проектов общего инстанса.
- **Плюс:** полная обратная совместимость — отчёты только с `verdict:`/`status:` работают как
раньше; существующие тесты `TestCheckTestsPassed` зелёные без правок (кроме обновления reason-текста
«…verdict/status…» → «…verdict/status/result…»).
- **Минус/ограничение:** число распознаваемых имён поля растёт до трёх — формально шире поверхность
«случайного PASS». Митигируется тем, что negative-токен авторитетен и читается только frontmatter.
- **SHARED-риск:** изменение затрагивает enduro-trails наравне с orchestrator. Регресс по наборам
токенов недопустим → они заморожены; покрытие — `04-test-plan.yaml` (AC-04/AC-05).
- **Self-hosting:** деплой строго через `deploy-staging` (8501); прод-контейнер `orchestrator`
(8500) не перезапускать в рамках разработки/тестинга.
## Связи
- BRD/ТЗ: `docs/work-items/ORCH-047/01-brd.md`, `02-trz.md`.
- Канон гейтов и вердикт-парсер: `docs/architecture/README.md`.
- Промпт-производитель: `.openclaw/agents/tester.md` (`result: PASS|FAIL`).
- adr-0003 (staging-гейт) — обязательная страховка перед прод-деплоем self.

View File

@@ -0,0 +1,10 @@
# Технические риски — ORCH-047
| # | Риск | Вероятность | Влияние | Митигация |
|---|------|-------------|---------|-----------|
| R-1 | Регресс набора токенов ломает enduro-trails (SHARED-гейт, общий прод-инстанс) | Низкая | Высокое | Наборы `_TESTS_NEGATIVE_TOKENS`/`_TESTS_POSITIVE_TOKENS` **заморожены** (не трогать). Покрытие AC-05 на реальных формах ET-001…ET-014 + ORCH-016. |
| R-2 | Новое поле `result:` расширяет поверхность ложного PASS | Низкая | Среднее | Negative-токен авторитетен (проверяется первым, перебивает positive). Читается только frontmatter, не проза (AC-03, AC-06, AC-07). |
| R-3 | Парсер бросает исключение на битом вводе → падение `_run_qg` | Низкая | Высокое | Defensive-контракт сохранён: любой ввод (нет frontmatter / битый YAML / не-mapping / пустой) → `(False, reason)`, никогда raise (AC-08). |
| R-4 | Незаметное изменение реестра гейтов | Очень низкая | Среднее | Сигнатура, имя гейта и `QG_CHECKS` неизменны; снапшот `tests/test_qg_registry_snapshot.py` зелёный (AC-10). |
| R-5 | Self-hosting: деплой роняет прод-контейнер всех проектов | Низкая | Высокое | Деплой только через `deploy-staging` (8501); прод `orchestrator` (8500) не перезапускать в dev/test (CLAUDE.md, adr-0003). |
| R-6 | Изменение поведения без обновления golden-source доки → REQUEST_CHANGES на review | Средняя | Низкое | ADR-001 заведён; `docs/architecture/README.md` (вердикт-парсер) обновлён архитектором; `CHANGELOG.md` — дев в том же PR (AC-11). |

View File

@@ -0,0 +1,62 @@
---
type: review
work_item_id: ORCH-047
verdict: APPROVED
version: 3
---
# Review ORCH-047
## Summary
Гейт `check_tests_passed` (через `_parse_tests_verdict`) теперь читает `result:` наравне с
`verdict:`/`status:`. Реализация точно соответствует ТЗ (`02-trz.md`), ADR-001 и критериям
приёмки. Независимый прогон: `pytest tests/ -q`**442 passed**; снапшот реестра гейтов не
изменился. Документация (README, ADR-001, CHANGELOG) обновлена в том же PR. Блокеров и
must-fix нет → APPROVED.
## Findings
### P0 — Blocker
- нет
### P1 — Must fix
- нет
### P2 — Should fix
- нет
### P3 — Nice-to-have
- [ ] Докстринг `check_tests_passed` (≈стр. 184) по-прежнему говорит «Gate the testing ->
deploy transition», тогда как фактический переход — `testing → deploy-staging`.
Несоответствие предсуществующее, этим PR не введено; чистая косметика, не блокирует.
## Соответствие ТЗ и AC
- **ТЗ §2** — все 10 правил поведения реализованы: чтение `result:` (стр. 261, нормализация
`str(...).upper().strip()` + защита от `None`); все три пусты → корректная reason-строка
«...verdict/status/result...» (стр. 263264); объединённая строка `fields = "{verdict}
{status} {result}"` (стр. 267); negative-токен проверяется ПЕРВЫМ и авторитетен
(стр. 268270); positive (стр. 271273); fallback на нераспознанные (стр. 275279).
Наборы `_TESTS_NEGATIVE_TOKENS`/`_TESTS_POSITIVE_TOKENS` не тронуты. ✅
- **ТЗ §4/§5/§6** — сигнатура `check_tests_passed`, имя гейта, `QG_CHECKS`, HTTP-API, схема БД
не изменены. Снапшот `tests/test_qg_registry_snapshot.py` зелёный (AC-10). ✅
- **AC-01..AC-09** — покрыты новыми кейсами в `TestCheckTestsPassed`: `result: PASS/FAIL`,
авторитетность negative между полями (`verdict: BLOCKED`, `status: failed` поверх
`result: PASS`), `result: ready-to-deploy`, отсутствие машинных полей (reason упоминает
`result`). Легаси-кейсы остались зелёными без правок логики (AC-05). ✅
- **AC-12** — `pytest tests/ -q` → 442 passed (независимый прогон ревьюера). ✅
## Соответствие ADR
- ADR-001 (`06-adr/ADR-001-result-field-in-tests-gate.md`): решение «три равноправных поля,
токены заморожены, negative авторитетен, реестр/сигнатура неизменны» полностью отражено
в коде.
- Глобальный ADR обоснованно не требуется (изменение не добавляет гейт/стадию/компонент,
не меняет топологию) — согласуется с конвенцией CLAUDE.md. SHARED-риск общего инстанса
(orchestrator + enduro-trails) учтён: токены заморожены, обратная совместимость покрыта
тестами.
## Документация
ОБНОВЛЕНА в том же PR (правило 2/6 CLAUDE.md, AC-11):
- `docs/architecture/README.md` — строка вердикт-парсера: для testing-гейта перечислены
`result:`/`verdict:`/`status:` + пометка про авторитетность negative. ✅
- `docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md` — заведён. ✅
- `CHANGELOG.md` — запись в `Fixed` про ORCH-047. ✅

View File

@@ -0,0 +1,78 @@
---
type: test-report
work_item_id: ORCH-047
result: PASS
---
# Test Report — ORCH-047
`check_tests_passed` / `_parse_tests_verdict` читает `result:` наравне с `verdict:`/`status:`.
## Окружение
- Python: 3.12.13
- pytest: 8.3.3
- Ветка: feature/ORCH-047-check-tests-passed-gate-must-r
- Среда: dev worktree (прод-контейнер `orchestrator` :8500 не затронут)
- Дата: 2026-06-05
## Smoke test API (prod :8500, read-only)
| Endpoint | Результат |
|----------|-----------|
| `GET /health` | `{"status":"ok","service":"orchestrator"}` — OK |
| `GET /status` | 200, активные задачи отдаются (ORCH-047 в testing) — OK |
| `GET /queue` | 200, counts/breaker/preflight в норме (running:1, failed:0) — OK |
## Результаты (план `04-test-plan.yaml`)
| TC ID | Описание | Тест | Результат |
|-------|----------|------|-----------|
| TC-01 | `result: PASS` без verdict/status → PASS (AC-01) | `test_result_pass_passes` | PASS |
| TC-02 | `result: FAIL` → FAIL, reason содержит FAIL (AC-02) | `test_result_fail_fails` | PASS |
| TC-03 | `result: PASS` + `verdict: BLOCKED` → negative авторитетен → FAIL (AC-03) | `test_result_pass_but_verdict_blocked_fails` | PASS |
| TC-04 | `result: PASS` + `status: failed` → FAIL (AC-03) | `test_result_pass_but_status_failed_fails` | PASS |
| TC-05 | `result: ready-to-deploy` → PASS (AC-04) | `test_result_ready_to_deploy_passes` | PASS |
| TC-06 | Легаси `verdict: PASS` → PASS, без регресса (AC-05) | `test_verdict_pass_passes` | PASS |
| TC-07 | `verdict: BLOCKED` + проза «23 passed» → FAIL (AC-05) | `test_passed_count_in_body_but_blocked_verdict_fails` | PASS |
| TC-08 | Нет машинных полей, проза «Result: PASS» → FAIL (AC-06) | `test_no_machine_field_reason_mentions_result` | PASS |
| TC-09 | Нет frontmatter → FAIL (AC-07) | `test_no_frontmatter_fails` | PASS |
| TC-10 | Битый YAML → FAIL без исключения (AC-08) | `test_invalid_yaml_fails_no_exception` | PASS |
| TC-11 | Отчёт отсутствует → FAIL «not found» (AC-09) | `test_no_report` | PASS |
| TC-12 | Реестр `QG_CHECKS` неизменен (AC-10) | `test_qg_registry_snapshot.py` (3 теста) | PASS |
| TC-13 | Полный регресс зелёный (AC-05, AC-12) | `pytest tests/` | PASS |
## Покрытие критериев приёмки
| AC | Статус |
|----|--------|
| AC-01 `result: PASS` проходит | PASS |
| AC-02 `result: FAIL` откатывает | PASS |
| AC-03 negative авторитетен между полями | PASS |
| AC-04 positive в любом из трёх полей → PASS | PASS |
| AC-05 обратная совместимость (TestCheckTestsPassed) | PASS |
| AC-06 ни одно поле не задано → FAIL | PASS |
| AC-07 только проза без frontmatter → FAIL | PASS |
| AC-08 битый YAML → FAIL без raise | PASS |
| AC-09 отчёт отсутствует → FAIL | PASS |
| AC-10 реестр гейтов неизменен | PASS |
| AC-11 ADR/README/CHANGELOG обновлены | PASS |
| AC-12 полный регресс зелёный | PASS |
AC-11 проверено вручную:
- `docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md` — присутствует.
- `docs/architecture/README.md` — строка вердикт-парсера перечисляет `result:`/`verdict:`/`status:`.
- `CHANGELOG.md` — запись `fix:` про ORCH-047.
## Вывод pytest
```
tests/test_qg.py ............................... TestCheckTestsPassed (все PASS,
включая новые test_result_* и легаси-кейсы)
tests/test_qg_registry_snapshot.py::test_tc20_qg_callables_unchanged PASSED
tests/test_qg_registry_snapshot.py::test_tc20_stage_transitions_unchanged PASSED
...
======================== 442 passed, 1 warning in 7.77s ========================
```
(1 warning — предсуществующий PydanticDeprecatedSince20 в `src/config.py`, не связан с ORCH-047.)
## Итог
PASS — все 13 TC и 12 AC выполнены, полный регресс зелёный (442 passed), smoke OK,
реестр гейтов не изменён. Задача готова к стадии deploy-staging.

View File

@@ -0,0 +1,83 @@
---
staging_status: FAILED
timestamp: 2026-06-05T21:30:45Z
base_url: http://localhost:8501
mode: stub
result: 9/10 checks PASS
exit_code: 1
---
# Staging Gate Log — ORCH-047
Staging test suite **FAILED**: 9/10 checks passed, exit code 1.
## Verdict
The live staging service on `:8501` is healthy and the full E2E pipeline ran
correctly against the **sandbox** project (issue created → webhook accepted →
branch created in `orchestrator-sandbox` → analyst job enqueued → cleanup OK).
The single failing check is **B6 — Registry isolation**: the project registry as
seen by the test harness still contains the production projects
(`enduro-trails`, `ORCH`) and does **not** isolate to the sandbox project only.
This violates the staging isolation requirement (CLAUDE.md: "staging — только
sandbox-проект"). Because the staging gate returned a non-zero exit code, the
machine verdict is `FAILED` and the task is rolled back to `development`.
### Notes for follow-up (development)
- B6 imports `src.projects.known_plane_project_ids()` and asserts the registry
contains the sandbox id (`8c5a3025-…`) while the prod ids
(`7a79f0a9-…` ET, `8da6aa25-…` ORCH) are absent. It observed
`sandbox=NO, prod-ET=YES(BAD!), prod-ORCH=YES(BAD!)`.
- This is a staging-environment / registry-isolation signal, not a verdict on the
ORCH-047 code change itself (which targets the `check_tests_passed` gate).
Investigate whether the staging container's isolated project registry env is
loaded, or whether the harness's in-process registry import is reading the host
(`/repos/orchestrator`) prod env instead of the container's env.
- Deployer did **not** modify any production infrastructure, registry, `.env`,
or `docker-compose.yml` to alter this result (per deployer mandate).
## Full test output
```
============================================================
ORCH-33 Staging Check Suite
base_url : http://localhost:8501
mode : stub
utc_time : 2026-06-05T21:30:45.071676+00:00
============================================================
[Block A] SMOKE
✓ PASS A1 GET /health → 200 status=ok [HTTP 200, body={'status': 'ok', 'service': 'orchestrator'}]
✓ PASS A2 GET /queue → 200 with counts/max_concurrency/resilience [HTTP 200, keys=['counts', 'max_concurrency', 'poll_interval', 'resilience', 'recent']]
✓ PASS A3 ORCH_STAGING=true (not prod) [ORCH_STAGING=true]
[Block B] ACCESS
✓ PASS B4 Plane: sandbox project accessible [HTTP 200, found 5 project(s), sandbox=YES]
✓ PASS B5 Gitea: orchestrator-sandbox accessible, push=true [HTTP 200, permissions={'admin': True, 'push': True, 'pull': True}]
✗ FAIL B6 Registry: sandbox present, prod ET/ORCH absent [sandbox=NO, prod-ET=YES(BAD!), prod-ORCH=YES(BAD!)]
[Block C] E2E (mode=stub)
· C7: Creating issue in SANDBOX project...
✓ PASS C7 Create issue in Plane SANDBOX [HTTP 201, issue_id=5040c202-592f-45d0-9463-ca1e9944e6ba]
· C8: Triggering pipeline via POST /webhook/plane ...
· Using HMAC signature (secret len=40)
✓ PASS C8 Trigger pipeline via /webhook/plane [HTTP 200, resp={'status': 'accepted'}]
· C9a: Polling for branch in orchestrator-sandbox (up to 60s)...
✓ PASS C9a Branch appears in orchestrator-sandbox [branch=feature/SANDBOX-010-staging-check-e2e-20260605t213]
· C9b: Checking staging job queue for analyst job (up to 30s)...
· (Plane comment check skipped: bot-tokens not added to SANDBOX project)
✓ PASS C9b Analyst job enqueued in staging queue [job_id=6, status=queued, agent=analyst]
[CLEANUP]
✓ PASS CLEANUP: deleted branch 'feature/SANDBOX-010-staging-check-e2e-20260605t213' (HTTP 204)
✓ PASS CLEANUP: deleted Plane issue 5040c202-592f-45d0-9463-ca1e9944e6ba (HTTP 204)
· CLEANUP DB: no task row found for plane_id=5040c202-592f-45d0-9463-ca1e9944e6ba
· CLEANUP DB dedup: no such table: events_dedup
============================================================
RESULT: 9/10 checks PASS
============================================================
EXIT_CODE=1
```

View File

@@ -121,6 +121,15 @@ class Settings(BaseSettings):
log_keep_max: int = 500
# ORCH-045: quality-gate CI poll/retry. check_ci_green polls the Gitea
# combined commit status up to ci_poll_max_attempts times, sleeping
# ci_poll_interval_s between attempts, to ride out a transient pending
# state right after the developer push (race fix, see ORCH-017).
# ci_poll_max_attempts -> max status polls (env ORCH_CI_POLL_MAX_ATTEMPTS)
# ci_poll_interval_s -> seconds between polls (env ORCH_CI_POLL_INTERVAL_S)
ci_poll_max_attempts: int = 12
ci_poll_interval_s: int = 10
# Telegram notifications
telegram_bot_token: str = ""
telegram_chat_id: str = ""

View File

@@ -1,6 +1,7 @@
"""Quality Gate checks — real implementations using Gitea/Plane API and filesystem."""
import os
import time
import logging
import subprocess
import httpx
@@ -82,23 +83,65 @@ def check_ci_green(repo: str, branch: str) -> tuple[bool, str]:
"""
Check if CI status is green for branch via Gitea API.
GET /repos/{owner}/{repo}/commits/{branch}/status
ORCH-045: polling with retry to fix a race condition. The gate used to do a
single status read right after the developer push; if CI was still ``pending``
for the first 1-3s (real case ORCH-017: polled 17:58:54 -> pending, CI went
green 17:58:55) the gate returned False once and the task stalled silently.
Behaviour now:
* ``success`` -> (True, "CI green") immediately.
* ``failure`` / ``error`` -> (False, "CI state: <state>") immediately
(CI is red, retrying is pointless).
* ``pending`` / unknown -> sleep ``ci_poll_interval_s`` and poll again,
up to ``ci_poll_max_attempts`` times.
* still pending after all attempts -> (False, "CI still pending after <T>s").
* 404 -> (False, "Branch not found or no status").
* transient httpx errors -> logged and retried within the attempt budget;
if every attempt errors -> (False, "API error: <e>").
"""
owner = settings.gitea_owner
url = f"{GITEA_BASE}/repos/{owner}/{repo}/commits/{branch}/status"
try:
resp = httpx.get(url, headers=GITEA_HEADERS, timeout=10)
if resp.status_code == 404:
return False, f"Branch '{branch}' not found or no status"
resp.raise_for_status()
data = resp.json()
state = data.get("state", "unknown")
if state == "success":
return True, "CI green"
return False, f"CI state: {state}"
except httpx.HTTPError as e:
logger.error(f"Gitea API error checking CI: {e}")
return False, f"API error: {e}"
attempts = settings.ci_poll_max_attempts
interval = settings.ci_poll_interval_s
last_state = "unknown"
last_error: Exception | None = None
for i in range(1, attempts + 1):
try:
resp = httpx.get(url, headers=GITEA_HEADERS, timeout=10)
if resp.status_code == 404:
return False, f"Branch '{branch}' not found or no status"
resp.raise_for_status()
data = resp.json()
last_state = data.get("state", "unknown")
last_error = None
if last_state == "success":
return True, "CI green"
if last_state in ("failure", "error"):
return False, f"CI state: {last_state}"
# non-terminal (pending / unknown / other) -> retry below
except httpx.HTTPError as e:
last_error = e
logger.error(f"check_ci_green: attempt {i}/{attempts} API error: {e}")
if i < attempts:
if last_error is not None:
logger.info(
f"check_ci_green: attempt {i}/{attempts}, error, retrying in {interval}s"
)
else:
logger.info(
f"check_ci_green: attempt {i}/{attempts}, state={last_state}, "
f"retrying in {interval}s"
)
time.sleep(interval)
if last_error is not None:
return False, f"API error: {last_error}"
return False, f"CI still pending after {attempts * interval}s"
def check_review_approved(repo: str, pr_number: int) -> tuple[bool, str]:
@@ -145,8 +188,11 @@ def check_tests_passed(repo: str, work_item_id: str, branch: str | None = None)
explicitly marked `verdict: BLOCKED` / `status: blocked` but whose prose mentioned
"23 passed" / "✅ PASS" / "All checks passed" was treated as a pass, and an
unfinished feature reached Done. This mirrors check_reviewer_verdict (S-5) and
check_deploy_status (БАГ 8): read ONLY the YAML frontmatter `verdict:` / `status:`
fields, never the body.
check_deploy_status (БАГ 8): read ONLY the YAML frontmatter, never the body.
ORCH-047: the machine verdict is read from any of three equal-rank frontmatter
fields — `result:` (canonical, what the tester prompt emits), `verdict:` or
`status:` (legacy / enduro-trails). See _parse_tests_verdict.
File: docs/work-items/<work_item_id>/13-test-report.md
"""
@@ -179,15 +225,20 @@ _TESTS_POSITIVE_TOKENS = ("PASSED", "PASS", "READY-TO-DEPLOY", "READY_TO_DEPLOY"
def _parse_tests_verdict(content: str) -> tuple[bool, str]:
"""Map a 13-test-report.md body to a quality-gate verdict by reading ONLY the
machine-readable `verdict:` (and corroborating `status:`) YAML frontmatter fields.
machine-readable YAML frontmatter fields — never the prose body.
Three equal-rank fields are accepted (ORCH-047): `result:` (the canonical field
the tester prompt `.openclaw/agents/tester.md` is told to emit, `result: PASS|FAIL`),
plus `verdict:` and `status:` (legacy / enduro-trails ET-001..ET-014). ANY single
non-empty field is sufficient. Token sets are frozen for backward compatibility.
Rules:
- No frontmatter / bad YAML / neither field present -> (False, reason).
- A negative token (BLOCKED/FAILED/...) in verdict OR status -> (False) and is
authoritative (ET-013 main case: verdict BLOCKED wins over any prose PASS).
- Otherwise a positive token (PASS/PASSED/READY-TO-DEPLOY/...) in verdict OR
status -> (True).
- Anything else (unrecognized / empty verdict) -> (False, reason).
- No frontmatter / bad YAML / none of the three fields present -> (False, reason).
- A negative token (BLOCKED/FAILED/...) in ANY field -> (False) and is
authoritative (ET-013 main case: verdict BLOCKED wins over any prose PASS, and
beats a positive token in another field).
- Otherwise a positive token (PASS/PASSED/READY-TO-DEPLOY/...) in ANY field -> (True).
- Anything else (fields set but unrecognized) -> (False, reason).
"""
import yaml
@@ -207,19 +258,25 @@ def _parse_tests_verdict(content: str) -> tuple[bool, str]:
verdict = str(fm.get("verdict", "") or "").upper().strip()
status = str(fm.get("status", "") or "").upper().strip()
result = str(fm.get("result", "") or "").upper().strip()
if not verdict and not status:
return False, "No machine-readable verdict/status in test report frontmatter"
if not verdict and not status and not result:
return False, "No machine-readable verdict/status/result in test report frontmatter"
fields = f"{verdict} {status}"
value = verdict or status or result
fields = f"{verdict} {status} {result}"
for neg in _TESTS_NEGATIVE_TOKENS:
if neg in fields:
return False, f"Test verdict: {verdict or status} ({neg})"
return False, f"Test verdict: {value} ({neg})"
for pos in _TESTS_POSITIVE_TOKENS:
if pos in fields:
return True, f"Test verdict: {verdict or status} (PASS)"
return True, f"Test verdict: {value} (PASS)"
return False, f"No recognized PASS verdict in frontmatter (verdict={verdict!r}, status={status!r})"
return (
False,
f"No recognized PASS verdict in frontmatter "
f"(verdict={verdict!r}, status={status!r}, result={result!r})",
)
def check_analysis_approved(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:

View File

@@ -93,38 +93,82 @@ class TestCheckArchitectureDone:
assert passed is False
def _ci_status_resp(state, status_code=200):
"""Build a MagicMock httpx response for the Gitea combined-status endpoint."""
mock_resp = MagicMock()
mock_resp.status_code = status_code
mock_resp.json.return_value = {"state": state}
mock_resp.raise_for_status = MagicMock()
return mock_resp
class TestCheckCIGreen:
"""ORCH-045: check_ci_green now polls with retry to ride out a transient
`pending` right after the developer push (race fix, see ORCH-017)."""
@patch("src.qg.checks.time.sleep")
@patch("src.qg.checks.httpx.get")
def test_ci_success(self, mock_get):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"state": "success"}
mock_resp.raise_for_status = MagicMock()
mock_get.return_value = mock_resp
def test_ci_success_first_attempt(self, mock_get, mock_sleep):
mock_get.return_value = _ci_status_resp("success")
passed, reason = check_ci_green("enduro-trails", "feature/ET-001-test")
assert passed is True
assert "green" in reason.lower()
assert mock_get.call_count == 1
mock_sleep.assert_not_called()
@patch("src.qg.checks.time.sleep")
@patch("src.qg.checks.httpx.get")
def test_ci_pending(self, mock_get):
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"state": "pending"}
mock_resp.raise_for_status = MagicMock()
mock_get.return_value = mock_resp
def test_ci_pending_then_success(self, mock_get, mock_sleep):
# pending on the 1st poll, green on the 2nd -> success after one retry.
mock_get.side_effect = [
_ci_status_resp("pending"),
_ci_status_resp("success"),
]
passed, reason = check_ci_green("enduro-trails", "feature/ET-001-test")
assert passed is True
assert "green" in reason.lower()
assert mock_get.call_count == 2
assert mock_sleep.call_count == 1 # slept once between the two polls
@patch("src.qg.checks.time.sleep")
@patch("src.qg.checks.httpx.get")
def test_ci_failure_no_retry(self, mock_get, mock_sleep):
# CI is red -> terminal, return immediately without sleeping/retrying.
mock_get.return_value = _ci_status_resp("failure")
passed, reason = check_ci_green("enduro-trails", "feature/ET-001-test")
assert passed is False
assert "failure" in reason
assert mock_get.call_count == 1
mock_sleep.assert_not_called()
@patch("src.qg.checks.time.sleep")
@patch("src.qg.checks.httpx.get")
def test_ci_branch_not_found(self, mock_get):
def test_ci_pending_exhausts_attempts(self, mock_get, mock_sleep):
# Always pending -> after ci_poll_max_attempts polls return an explicit
# (False, "...pending...") so the operator sees the reason (no silent stall).
from src.qg.checks import settings as checks_settings
mock_get.return_value = _ci_status_resp("pending")
passed, reason = check_ci_green("enduro-trails", "feature/ET-001-test")
assert passed is False
assert "pending" in reason.lower()
assert mock_get.call_count == checks_settings.ci_poll_max_attempts
@patch("src.qg.checks.time.sleep")
@patch("src.qg.checks.httpx.get")
def test_ci_branch_not_found(self, mock_get, mock_sleep):
mock_resp = MagicMock()
mock_resp.status_code = 404
mock_get.return_value = mock_resp
passed, reason = check_ci_green("enduro-trails", "nonexistent")
assert passed is False
assert "not found" in reason.lower()
assert mock_get.call_count == 1
class TestCheckReviewApproved:
@@ -278,6 +322,64 @@ class TestCheckTestsPassed:
assert passed is False
assert "not found" in reason.lower()
# --- ORCH-047: `result:` is read as an equal-rank machine field ---
def test_result_pass_passes(self, setup_work_item_dir):
# TC-01 / AC-01: canonical tester field `result: PASS` (no verdict/status).
self._write(
setup_work_item_dir,
"---\ntype: test-report\nresult: PASS\n---\n\n# Test Report\n",
)
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is True
assert "PASS" in reason
def test_result_fail_fails(self, setup_work_item_dir):
# TC-02 / AC-02: `result: FAIL` (no verdict/status) -> rollback, reason has FAIL.
self._write(setup_work_item_dir, "---\nresult: FAIL\n---\n\nbody\n")
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is False
assert "FAIL" in reason
def test_result_pass_but_verdict_blocked_fails(self, setup_work_item_dir):
# TC-03 / AC-03: negative in another field is authoritative over result: PASS.
self._write(
setup_work_item_dir,
"---\nresult: PASS\nverdict: BLOCKED\n---\n\n23 passed\n",
)
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is False
assert "BLOCKED" in reason
def test_result_pass_but_status_failed_fails(self, setup_work_item_dir):
# TC-04 / AC-03: status: failed authoritative over result: PASS.
self._write(
setup_work_item_dir,
"---\nresult: PASS\nstatus: failed\n---\n\nbody\n",
)
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is False
assert "FAILED" in reason
def test_result_ready_to_deploy_passes(self, setup_work_item_dir):
# TC-05 / AC-04: positive token without the word PASS, in result field.
self._write(
setup_work_item_dir,
"---\nresult: ready-to-deploy\n---\n\nbody\n",
)
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is True
def test_no_machine_field_reason_mentions_result(self, setup_work_item_dir):
# AC-06: none of result/verdict/status -> fail; reason now lists result too.
self._write(
setup_work_item_dir,
"---\ntype: test-report\nversion: 1\n---\n\nResult: PASS\n",
)
passed, reason = check_tests_passed("enduro-trails", "ET-001")
assert passed is False
assert "result" in reason.lower()
class TestCheckDeployStatus:
"""BUG 8: deploy -> done must be gated on the deployer's machine-readable