Files
enduro-trails/docs/work-items/ET-015/12-review.md
claude-bot d501bcbbc4
All checks were successful
CI / lint (push) Successful in 4s
CI / lint (pull_request) Successful in 4s
CI / test (push) Successful in 12s
CI / build (push) Successful in 3s
CI / test (pull_request) Successful in 12s
CI / build (pull_request) Successful in 2s
reviewer(ET): auto-commit from reviewer run_id=104
2026-06-05 15:37:03 +00:00

153 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: review
work_item_id: ET-015
verdict: APPROVED
version: 1
---
# Review ET-015 — Healthcheck enduro-trails-app (python urllib one-liner)
**Branch:** `feature/ET-015-healthcheck-enduro-trails-app-`
**Base:** `main`
**Reviewer:** agent:reviewer
**Date:** 2026-06-05
## Что проверял
- TRZ: `docs/work-items/ET-015/02-trz.md` (особенно §3.1, §3.2, §3.3, R-1..R-5)
- AC: `docs/work-items/ET-015/03-acceptance-criteria.md` (AC-01..AC-10)
- ADR: `docs/work-items/ET-015/06-adr/ADR-020-healthcheck-via-python-urllib.md`
- Глобальный ADR-индекс: `docs/architecture/adr/README.md`
- PR diff (`git diff main..HEAD`): `docker-compose.yml`, `CHANGELOG.md`,
`docs/architecture/adr/README.md`, артефакты `docs/work-items/ET-015/**`,
`tests/static/test_healthcheck_compose.py`,
`tests/unit/test_healthcheck_oneliner.py`.
- Запуск тестов: `pytest tests/static/test_healthcheck_compose.py
tests/unit/test_healthcheck_oneliner.py -v` → **16 passed**.
## Соответствие ТЗ
| Пункт TRZ | Ожидание | Факт в diff | Статус |
|-----------|----------|-------------|--------|
| §3.1 | YAML-массив `["CMD","python","-c", "<one-liner>"]` + `start_period: 20s`, `interval/timeout/retries` сохранены | `docker-compose.yml` lines 2231 — байт-в-байт совпадает с §3.1 ADR-020 | ✓ |
| §3.2 | Dockerfile НЕ меняется | `git diff main..HEAD -- Dockerfile` пуст | ✓ |
| §3.3 | `src/api/main.py` НЕ меняется | `git diff main..HEAD -- src/api/main.py` пуст | ✓ |
| R-1 | Затронут только `app.healthcheck`, прочие поля сервиса не тронуты | Подтверждено diff'ом — ports/volumes/environment не сдвинуты | ✓ |
| R-2 | Изменение не требует `docker compose build` | Образ не меняется, команда исполняется существующим `python` интерпретатором | ✓ |
| R-3 | Никаких ENV для пути healthcheck | URL зашит литералом | ✓ |
| R-4 | ADR в `06-adr/` + запись в CHANGELOG | `ADR-020-healthcheck-via-python-urllib.md` + Unreleased/Fixed в `CHANGELOG.md` | ✓ |
| R-5 | YAML валидный | `yaml.safe_load(open("docker-compose.yml"))` парсит без ошибок (проверено) | ✓ |
## Соответствие Acceptance Criteria
| AC | Тест | Результат |
|----|------|-----------|
| AC-03 «нет curl в healthcheck» | ST-01 (`test_st01_healthcheck_does_not_use_curl`) | PASS |
| AC-04 «Dockerfile не ставит curl/wget» | ST-02 (`test_st02_dockerfile_does_not_apt_install_curl_or_wget`) + IT-04 (manual) | PASS (static) |
| AC-05 «честно фиксирует unhealthy» | UT-02 (`test_ut02_returns_nonzero_when_port_unused`) + IT-03 (manual) | PASS (unit) |
| AC-06 «stdlib python one-liner» | ST-03, UT-01, UT-03 (4 параметризации: 301/404/500/503) | PASS |
| AC-07 «внутренний timeout < внешнего» | ST-04 (`test_st04_internal_timeout_less_than_external`) — `3 < 5` | PASS |
| AC-08 «/api/health не сломан» | `git diff main..HEAD -- src/api/main.py` пуст; E2E-02 (manual) | PASS (static) |
| AC-09 «CHANGELOG обновлён» | ST-06 (`test_st06_changelog_mentions_et015`) | PASS |
| AC-10 «ADR зафиксирован» | ST-07 (`test_st07_adr_exists`) + ручная проверка содержимого ADR-020 | PASS |
| AC-01, AC-02 «healthy после деплоя / стабилен 10 минут» | IT-01/IT-02/E2E-01 — оператор/deployer | Pending (вне review) |
Замечание: AC-01/AC-02 закрываются только на live-среде (deployer/ops после
`make deploy-test`); это явно зафиксировано в плане тестов (`done_when`).
Review не блокирует — статические + unit-проверки полностью покрывают всё,
что можно проверить из ветки.
## Соответствие ADR-020
- §«Решение» п.1 — YAML-блок 1:1 совпадает с фактическим `docker-compose.yml`.
- §«Решение» п.2 — Dockerfile не тронут ✓.
- §«Решение» п.3 — `main.py` не тронут ✓.
- §«Решение» п.4 — `gps-collector` healthcheck не получает (в diff'е сервис
не меняется) ✓.
- §«Решение» п.5 — `CHANGELOG.md` Unreleased/Fixed содержит ET-015 + строку
`fix(infra): use python urllib for container healthcheck (ET-015)` ✓.
- Глобальный индекс ADR (`docs/architecture/adr/README.md`) пополнен строкой
ADR-020 ✓ (соблюдено процессное требование).
- Альтернативы B/C/D/E явно отклонены и не «протекли» в реализацию (curl/wget
не появились, отдельный `scripts/healthcheck.py` не создан, `HEALTHCHECK`
директива в Dockerfile не добавлена) ✓.
## Качество кода
- **YAML.** Используется `CMD` (массив), а не `CMD-SHELL`. Корректно: Docker
выполняет `exec`-ом без shell-парсинга, экранирование не нужно.
- **One-liner.** `import urllib.request, sys; sys.exit(0 if
urllib.request.urlopen(URL, timeout=3).status == 200 else 1)` —
компактно, без побочных эффектов, исключения корректно превращаются в
ненулевой exit code, что и нужно Docker'у.
- **`start_period: 20s`** добавлен — оправдан в ADR/TRZ, защищает от ложных
фейлов в первые секунды старта uvicorn.
- **Diff минимален и хирургичен.** Затронут ровно один логический блок —
это и есть «minor-change» по классификации ADR-020 §«Классификация».
## Качество тестов
- **`tests/static/test_healthcheck_compose.py`** (10 тестов):
- 6 первичных (ST-01..ST-04, ST-06, ST-07) с явной привязкой к AC и
источникам правды в docstring.
- 3 регрессивных: проверка локального URL, наличие `start_period`,
параметризованная проверка инвариантов `interval ≥ 30`, `retries ≥ 3`
(защита ADR-020 «инвариант: параметры не уменьшаются»).
- Чёрный список сторонних пакетов (`requests/httpx/aiohttp/urllib3`)
через `\b<pkg>\b` — корректный приём против ложных совпадений
подстроками.
- **`tests/unit/test_healthcheck_oneliner.py`** (6 тестов):
- Ключевая фишка: код one-liner'а **читается из `docker-compose.yml`**
и URL подменяется через `_retarget()` — под тест уходит ровно та же
логика, что и в проде. Если в compose кто-то поменяет one-liner и
сломает контракт exit-code, эти тесты упадут.
- UT-01 проверяет `exit 0` на HTTP 200, UT-02 — `exit ≠ 0` при пустом
порту, UT-03 параметризован по 301/404/500/503 (защита от подмены
`== 200` на `< 400` или подобное).
- Мок-сервер на `http.server` — без внешних зависимостей, без флакки.
- Тесты **запущены локально** (`pytest -v`): **16 passed** за 2.89s.
## Findings
### P0 (blocker)
Нет.
### P1 (must-fix)
Нет.
### P2 (should-fix)
Нет.
### P3 (nice-to-have)
- **P3-1.** `CHANGELOG.md` исторически содержит **два** `## [Unreleased]`
заголовка (строки 6 и 151) — это унаследованная проблема репозитория,
PR ET-015 её не вносит и не усугубляет. Просто фиксирую — стоит когда-нибудь
устранить в отдельной задаче `docs:`. ST-06 на этом не ломается, потому что
ищет ET-015 в любой части файла, а не «строго в верхней Unreleased».
- **P3-2.** В TRZ §1 формулировка цели говорит про «HTTP-код **2xx** как
healthy», но §3.1 (и реализация) проверяют именно `status == 200`. На
практике `/api/health` отдаёт 200, и UT-03 явно фиксирует поведение
для 301/404/500/503 → unhealthy, что согласуется с ADR-020. Это
ожидаемое сужение, зафиксированное в ADR-020 §«Решение»; стоит лишь
иметь в виду как документационную «шероховатость» в TRZ. Менять
поведение **не нужно**: иначе сломается часть UT-03 (301).
- **P3-3.** `urllib.request.urlopen(...).status` — объект ответа не
закрывается (нет `with`). Для одноразового процесса healthcheck это
безопасно (process завершится через `sys.exit`), но из стилистических
соображений можно когда-нибудь обернуть в `with`. Не блокирует
и не входит в скоуп ET-015.
## Вердикт
**APPROVED.** Все P0/P1/P2 пусты. Реализация 1-в-1 соответствует TRZ §3.1
и ADR-020 §«Решение»; не выходит за рамки BRD §6/§7; тесты адекватно
покрывают статические инварианты и поведение exit-кода one-liner'а.
Финальные AC-01/AC-02 закрываются на этапе deploy на mva154 — это
ожидаемо и зафиксировано в `04-test-plan.yaml::done_when`.
Можно передавать дальше (tester → deployer).