diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a43352..7b1e1bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,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. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 3eef0ce..4baebe5 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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) diff --git a/docs/work-items/ORCH-047/00-business-request.md b/docs/work-items/ORCH-047/00-business-request.md new file mode 100644 index 0000000..b591fb4 --- /dev/null +++ b/docs/work-items/ORCH-047/00-business-request.md @@ -0,0 +1,7 @@ +# Business Request: check_tests_passed: gate must read result: field from test report + +Work Item ID: ORCH-047 + +## Description + +TBD diff --git a/docs/work-items/ORCH-047/01-brd.md b/docs/work-items/ORCH-047/01-brd.md new file mode 100644 index 0000000..080b62d --- /dev/null +++ b/docs/work-items/ORCH-047/01-brd.md @@ -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`, строки 51–56 и 78–80) предписывает эмитить машиночитаемое поле **`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 заведён. diff --git a/docs/work-items/ORCH-047/02-trz.md b/docs/work-items/ORCH-047/02-trz.md new file mode 100644 index 0000000..5add9be --- /dev/null +++ b/docs/work-items/ORCH-047/02-trz.md @@ -0,0 +1,68 @@ +# ТЗ — ORCH-047: `_parse_tests_verdict` читает `result:` наравне с `verdict:`/`status:` + +## 1. Задействованные модули `src/` + +| Файл | Что меняется | +|------|--------------| +| `src/qg/checks.py` | Функция `_parse_tests_verdict` (стр. ~223–265). Добавить чтение поля `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 ...: ")` (никогда не 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: <значение> ()")`. **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) не перезапускать в рамках разработки/тестинга. diff --git a/docs/work-items/ORCH-047/03-acceptance-criteria.md b/docs/work-items/ORCH-047/03-acceptance-criteria.md new file mode 100644 index 0000000..672cb90 --- /dev/null +++ b/docs/work-items/ORCH-047/03-acceptance-criteria.md @@ -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:** любой упавший тест. diff --git a/docs/work-items/ORCH-047/04-test-plan.yaml b/docs/work-items/ORCH-047/04-test-plan.yaml new file mode 100644 index 0000000..e442856 --- /dev/null +++ b/docs/work-items/ORCH-047/04-test-plan.yaml @@ -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 diff --git a/docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md b/docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md new file mode 100644 index 0000000..a5447c0 --- /dev/null +++ b/docs/work-items/ORCH-047/06-adr/ADR-001-result-field-in-tests-gate.md @@ -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`, строки 51–56, 78–80) предписывает тестеру + эмитить машиночитаемое поле **`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. diff --git a/docs/work-items/ORCH-047/10-tech-risks.md b/docs/work-items/ORCH-047/10-tech-risks.md new file mode 100644 index 0000000..45fa9ed --- /dev/null +++ b/docs/work-items/ORCH-047/10-tech-risks.md @@ -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). | diff --git a/docs/work-items/ORCH-047/12-review.md b/docs/work-items/ORCH-047/12-review.md new file mode 100644 index 0000000..d0b7dd0 --- /dev/null +++ b/docs/work-items/ORCH-047/12-review.md @@ -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...» (стр. 263–264); объединённая строка `fields = "{verdict} + {status} {result}"` (стр. 267); negative-токен проверяется ПЕРВЫМ и авторитетен + (стр. 268–270); positive (стр. 271–273); fallback на нераспознанные (стр. 275–279). + Наборы `_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. ✅ diff --git a/docs/work-items/ORCH-047/13-test-report.md b/docs/work-items/ORCH-047/13-test-report.md new file mode 100644 index 0000000..5c195cd --- /dev/null +++ b/docs/work-items/ORCH-047/13-test-report.md @@ -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. diff --git a/src/qg/checks.py b/src/qg/checks.py index 8c97ad5..3d5e789 100644 --- a/src/qg/checks.py +++ b/src/qg/checks.py @@ -188,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//13-test-report.md """ @@ -222,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 @@ -250,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]: diff --git a/tests/test_qg.py b/tests/test_qg.py index d5fe2c7..eb41680 100644 --- a/tests/test_qg.py +++ b/tests/test_qg.py @@ -322,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