analyst(ET): auto-commit from analyst run_id=101
This commit is contained in:
105
docs/work-items/ET-015/01-brd.md
Normal file
105
docs/work-items/ET-015/01-brd.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# BRD: Healthcheck enduro-trails-app падает: в контейнере нет curl
|
||||
|
||||
**Work Item:** ET-015
|
||||
**Тип:** Bugfix / Infrastructure
|
||||
**Приоритет:** Низкий (приложение работает) / Важно для мониторинга
|
||||
**Дата:** 2026-06-05
|
||||
**Запросил:** Слава
|
||||
|
||||
## 1. Контекст
|
||||
|
||||
Контейнер `enduro-trails-app-1` (запускается из репозитория `enduro-trails`)
|
||||
на тестовой среде `mva154` (https://openclaw.mva154.duckdns.org/enduro/)
|
||||
показывает в Docker статус `unhealthy` уже ~31 час с `FailingStreak=3762`,
|
||||
при том что само приложение работает:
|
||||
|
||||
- `curl снаружи :5556 → HTTP 200` (~7 мс отклик);
|
||||
- в логах живой трафик `200 OK`;
|
||||
- `RestartCount=0` (контейнер не перезапускался).
|
||||
|
||||
## 2. Корень проблемы
|
||||
|
||||
В `docker-compose.yml` healthcheck настроен как:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5556/api/health"]
|
||||
```
|
||||
|
||||
Базовый образ — `python:3.12-slim` (см. `Dockerfile`). В `slim`-варианте
|
||||
**нет** утилиты `curl`. Каждый цикл healthcheck завершается:
|
||||
|
||||
```
|
||||
exec: "curl": executable file not found in $PATH
|
||||
exit code = -1
|
||||
```
|
||||
|
||||
Docker трактует это как «проверка провалена» и через `retries=3` помечает
|
||||
контейнер `unhealthy`. На самом деле приложение здорово.
|
||||
|
||||
Дополнительный факт: эндпоинт `/api/health` **существует** в коде
|
||||
(`src/api/main.py:1224`, отдаёт `{"status": "ok", ...}`), так что
|
||||
двойной поломки (несуществующий путь) нет — проблема исключительно
|
||||
в отсутствии `curl`.
|
||||
|
||||
## 3. Бизнес-проблема
|
||||
|
||||
1. **Ложные алерты в мониторинге.** Любая система оповещений, опирающаяся
|
||||
на `docker inspect ... .State.Health.Status`, будет постоянно кричать
|
||||
об инциденте, который не существует.
|
||||
2. **Эрозия доверия к мониторингу.** Если `unhealthy` всегда ложный, его
|
||||
игнорируют — и пропустят настоящий инцидент, когда он случится.
|
||||
3. **Невозможность построения SLO/SLA.** Метрика «доступность контейнера»
|
||||
деградирована и непригодна для отчётности.
|
||||
|
||||
## 4. Цель
|
||||
|
||||
Healthcheck контейнера `app` должен **честно** отражать состояние
|
||||
приложения: `healthy`, когда HTTP-эндпоинт `/api/health` на `:5556`
|
||||
отвечает `200 OK`; `unhealthy`, когда не отвечает.
|
||||
|
||||
## 5. Стейкхолдеры
|
||||
|
||||
| Роль | Имя / Группа | Интерес |
|
||||
|------|--------------|---------|
|
||||
| Заказчик | Слава | Корректный мониторинг тестовой и будущей prod-среды |
|
||||
| Исполнитель | claude-bot | Реализация фикса |
|
||||
| Эксплуатация | mva154 host owner | Минимальный размер образа, никаких лишних пакетов |
|
||||
|
||||
## 6. Ограничения и нефункциональные требования
|
||||
|
||||
- **Размер образа** не должен заметно расти. Добавление `curl` через
|
||||
`apt-get install` тянет ~10 МБ зависимостей + слой APT-кэша →
|
||||
нежелательно. Предпочтительно использовать то, что уже есть в образе
|
||||
(Python).
|
||||
- **Время выполнения healthcheck** не должно превышать `timeout: 5s`
|
||||
(текущее значение в compose). Реальное время отклика `/api/health`
|
||||
~7 мс, запас огромный.
|
||||
- **Совместимость** с Docker Engine ≥ 20.10 (на mva154 стоит свежий).
|
||||
- **Никаких изменений** в логике приложения — эндпоинт `/api/health`
|
||||
уже существует и его поведение менять не нужно.
|
||||
|
||||
## 7. Out of scope
|
||||
|
||||
- Доработка содержимого `/api/health` (например, добавление проверки
|
||||
OSRM, тайлов, диска) — отдельный work item, если понадобится.
|
||||
- Healthcheck для сервиса `gps-collector` (batch profile) — у него
|
||||
нет открытого порта и `restart: "no"`, healthcheck неуместен.
|
||||
- Healthcheck-настройки на стороне Gitea Actions / CI.
|
||||
|
||||
## 8. Сценарий «как должно стать»
|
||||
|
||||
1. Образ собирается без добавления `curl`.
|
||||
2. `docker compose up -d app` поднимает контейнер.
|
||||
3. Через ≤ `interval * retries` (= 30s × 3 = 90s, с учётом
|
||||
`start_period` если задан) `docker inspect ... .State.Health.Status`
|
||||
возвращает `healthy`.
|
||||
4. Если приложение «зависает» (порт не отвечает) — healthcheck
|
||||
честно фиксирует `unhealthy` за то же окно.
|
||||
|
||||
## 9. Связи
|
||||
|
||||
- Затрагивает: `Dockerfile`, `docker-compose.yml`.
|
||||
- Не затрагивает: `src/api/`, `src/web/`, БД, тайлы.
|
||||
- Соседние ADR: глобальных архитектурных решений не требует —
|
||||
локальное инженерное решение, оформляется в `06-adr/` work-item.
|
||||
169
docs/work-items/ET-015/02-trz.md
Normal file
169
docs/work-items/ET-015/02-trz.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# ТЗ: Healthcheck enduro-trails-app — заменить curl на python-проверку
|
||||
|
||||
**Work Item:** ET-015
|
||||
**Базовый документ:** [01-brd.md](01-brd.md)
|
||||
**Версия:** 1.0
|
||||
**Дата:** 2026-06-05
|
||||
|
||||
## 1. Постановка
|
||||
|
||||
Заменить в `docker-compose.yml` (сервис `app`) healthcheck-команду
|
||||
так, чтобы она:
|
||||
- использовала средства, **уже доступные** в образе `python:3.12-slim`
|
||||
(т.е. интерпретатор `python3`), без установки дополнительных пакетов;
|
||||
- обращалась к `http://localhost:5556/api/health` и трактовала
|
||||
HTTP-код **2xx** как «healthy», любой иной отклик и любую ошибку
|
||||
соединения — как «unhealthy»;
|
||||
- укладывалась в `timeout: 5s`.
|
||||
|
||||
## 2. Текущее состояние
|
||||
|
||||
`docker-compose.yml`, строки 22–26:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:5556/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
`Dockerfile`: базовый образ `python:3.12-slim`. `curl` отсутствует.
|
||||
Установлен `pip`, доступен `python3` (и алиас `python`).
|
||||
|
||||
`src/api/main.py:1224`:
|
||||
|
||||
```python
|
||||
@app.get("/api/health")
|
||||
async def health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"db_path": DATA_PATH,
|
||||
"db_exists": os.path.exists(DATA_PATH),
|
||||
}
|
||||
```
|
||||
|
||||
Возвращает HTTP 200 + JSON. Менять не требуется.
|
||||
|
||||
## 3. Целевое состояние
|
||||
|
||||
### 3.1. Изменение в `docker-compose.yml`
|
||||
|
||||
Секция `healthcheck` сервиса `app` приводится к виду:
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test:
|
||||
- "CMD"
|
||||
- "python"
|
||||
- "-c"
|
||||
- "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:5556/api/health', timeout=3).status == 200 else 1)"
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
```
|
||||
|
||||
Пояснения:
|
||||
- `CMD` (а не `CMD-SHELL`) — никакого shell-парсинга, аргументы передаются
|
||||
как массив, экранирование не нужно.
|
||||
- `python` — алиас, имеющийся в `python:3.12-slim` (есть и `python3`,
|
||||
оба указывают на один интерпретатор).
|
||||
- `urllib.request.urlopen(..., timeout=3)` — стандартная библиотека,
|
||||
без зависимостей; внутренний `timeout=3` короче внешнего
|
||||
`timeout: 5s`, остаётся запас на старт интерпретатора.
|
||||
- `sys.exit(0 if ... == 200 else 1)` — корректное преобразование
|
||||
статуса HTTP в exit code. Любой raise (URLError, HTTPError, timeout)
|
||||
пробросится наверх, процесс завершится ненулевым кодом → `unhealthy`.
|
||||
- `start_period: 20s` — добавляется, чтобы Docker не считал ранние
|
||||
ошибки запуска приложения «провалом» healthcheck в окне старта.
|
||||
Uvicorn поднимается за < 2 c, 20 с — комфортный запас.
|
||||
|
||||
### 3.2. Изменения в `Dockerfile`
|
||||
|
||||
**Не требуются.** Добавлять `curl` через `apt-get` нельзя — раздувает
|
||||
образ и противоречит выбранному подходу.
|
||||
|
||||
### 3.3. Изменения в `src/api/main.py`
|
||||
|
||||
**Не требуются.** Эндпоинт `/api/health` существует и отдаёт 200.
|
||||
|
||||
## 4. Альтернативы (рассмотрены и отклонены)
|
||||
|
||||
| Вариант | Плюсы | Минусы | Решение |
|
||||
|---------|-------|--------|---------|
|
||||
| `apt-get install curl` в Dockerfile | Привычная команда | +~10 МБ к образу, новый APT-слой, противоречит slim-философии | Отклонено |
|
||||
| `wget --spider` | Однострочник | `wget` тоже отсутствует в `python:3.12-slim` (проверено: пакет `wget` не входит в slim) | Отклонено |
|
||||
| HEALTHCHECK в Dockerfile | Декларативно | Дублирует compose, при изменении нужно пересобирать образ | Отклонено, держим в compose |
|
||||
| Отдельный health-скрипт `scripts/healthcheck.py` | Чище YAML | Лишний файл для одной строки, мутит образ | Отклонено |
|
||||
|
||||
Принятый вариант: **inline python one-liner** через `urllib.request`.
|
||||
|
||||
## 5. Реализационные требования
|
||||
|
||||
### R-1. Изменение `docker-compose.yml`
|
||||
- В сервисе `app` секция `healthcheck` заменяется на конструкцию из
|
||||
п. 3.1.
|
||||
- Остальные параметры сервиса (ports, volumes, environment) не
|
||||
затрагиваются.
|
||||
|
||||
### R-2. Идемпотентность пересборки
|
||||
- Изменения не требуют ребилда образа (`docker compose build`).
|
||||
Достаточно `docker compose up -d app` для пересоздания контейнера
|
||||
с новой healthcheck-командой.
|
||||
- Допускается ребилд при необходимости — это не должно ломать сборку.
|
||||
|
||||
### R-3. Обратная совместимость
|
||||
- Никаких ENV-переменных, влияющих на путь healthcheck, не вводится.
|
||||
Адрес `http://localhost:5556/api/health` зашит в строку.
|
||||
(Локальный — `localhost` внутри контейнера; порт всегда 5556,
|
||||
как ENV `PORT` в Dockerfile.)
|
||||
|
||||
### R-4. Документация
|
||||
- В `docs/work-items/ET-015/06-adr/healthcheck-via-python.md` зафиксировать
|
||||
решение «использовать python-one-liner вместо curl». Автор ADR —
|
||||
следующий этап (Architecture), не Анализ.
|
||||
- Обновить `CHANGELOG.md` в секции «Unreleased» строкой формата
|
||||
`fix(infra): use python urllib for container healthcheck (ET-015)`.
|
||||
|
||||
### R-5. Линт и форматирование
|
||||
- YAML-валидность `docker-compose.yml` проверяется `make lint`.
|
||||
- Длина строки python one-liner допустима в YAML (нет лимита 120 для
|
||||
строковых значений multi-line array).
|
||||
|
||||
## 6. Тестирование
|
||||
|
||||
См. [04-test-plan.yaml](04-test-plan.yaml). Кратко:
|
||||
|
||||
- **integration-1**: после `docker compose up -d app` контейнер должен
|
||||
выйти в `healthy` за ≤ 120 с.
|
||||
- **integration-2**: при остановке uvicorn (или искусственном блоке
|
||||
порта) — за ≤ 120 с переходит в `unhealthy`.
|
||||
- **unit-1** (опционально): smoke-тест python-one-liner вне Docker
|
||||
через `python -c "..."` против поднятого локально `make dev`.
|
||||
|
||||
## 7. Деплой и откат
|
||||
|
||||
- Деплой: `make deploy-test` (как обычно). При деплое compose
|
||||
пересоздаст контейнер `enduro-trails-app-1`.
|
||||
- Проверка: `docker inspect enduro-trails-app-1 --format
|
||||
'{{.State.Health.Status}}'` → `healthy` в течение нескольких циклов
|
||||
(`interval=30s × 3 = 90s` плюс `start_period=20s`).
|
||||
- Откат: `git revert` коммита; `docker compose up -d app`. Старая
|
||||
(поломанная) healthcheck-команда вернётся, но сам сервис продолжит
|
||||
работать.
|
||||
|
||||
## 8. Риски
|
||||
|
||||
| Риск | Вероятность | Митигация |
|
||||
|------|-------------|-----------|
|
||||
| Python one-liner крэшится на каком-то Docker-движке из-за квотинга | низкая | YAML-массив `["CMD", "python", "-c", "..."]` — без shell, без экранирования |
|
||||
| Длинная строка усложняет редактирование | средняя | Использовать YAML block-scalar (`>-` или `|`) при необходимости, но в текущей форме строка читаемая |
|
||||
| Эндпоинт `/api/health` в будущем сделают «дорогим» и timeout=3s не хватит | низкая | Эндпоинт сейчас отдаёт ~7 мс; при изменении — пересмотр timeout |
|
||||
| На prod-среде iptables/сеть отличаются и localhost внутри контейнера ведёт себя иначе | очень низкая | `localhost` в network namespace контейнера = loopback контейнера, не зависит от хоста |
|
||||
|
||||
## 9. Definition of Ready (для следующего этапа)
|
||||
|
||||
- BRD прочитан, ТЗ согласовано.
|
||||
- Доступ к тестовой среде mva154 для проверки `docker inspect`.
|
||||
- `make deploy-test` и `docker compose` доступны из ветки.
|
||||
111
docs/work-items/ET-015/03-acceptance-criteria.md
Normal file
111
docs/work-items/ET-015/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Acceptance Criteria: ET-015
|
||||
|
||||
**Work Item:** ET-015 — Healthcheck enduro-trails-app
|
||||
**Базовые документы:** [01-brd.md](01-brd.md), [02-trz.md](02-trz.md)
|
||||
|
||||
## Формат
|
||||
|
||||
Каждый критерий записан как Gherkin-сценарий (`Given/When/Then`) и
|
||||
имеет уникальный идентификатор `AC-NN`. Все критерии — обязательные,
|
||||
если не указано иное.
|
||||
|
||||
---
|
||||
|
||||
## AC-01. Контейнер становится healthy после деплоя
|
||||
|
||||
**Given** на тестовой среде `mva154` смерджена ветка
|
||||
`feature/ET-015-healthcheck-enduro-trails-app-`
|
||||
**And** выполнен `make deploy-test`
|
||||
**When** проходит ≤ 120 секунд после `docker compose up -d app`
|
||||
**Then** `docker inspect enduro-trails-app-1 --format
|
||||
'{{.State.Health.Status}}'` возвращает `healthy`
|
||||
**And** `docker inspect ... --format '{{.State.Health.FailingStreak}}'`
|
||||
возвращает `0`.
|
||||
|
||||
## AC-02. Контейнер остаётся healthy при штатной работе
|
||||
|
||||
**Given** контейнер `enduro-trails-app-1` в статусе `healthy`
|
||||
**When** проходит 10 минут без вмешательства
|
||||
**Then** статус остаётся `healthy`
|
||||
**And** `FailingStreak == 0`
|
||||
**And** в `docker inspect ... --format '{{json .State.Health.Log}}'`
|
||||
последние 5 записей имеют `ExitCode: 0`.
|
||||
|
||||
## AC-03. Healthcheck не использует curl
|
||||
|
||||
**Given** ветка `feature/ET-015-healthcheck-enduro-trails-app-` смерджена
|
||||
**When** выполняется `grep -n curl docker-compose.yml`
|
||||
**Then** в выводе нет строки в секции `healthcheck` сервиса `app`
|
||||
содержащей `curl`.
|
||||
|
||||
## AC-04. Образ не растёт за счёт установки curl/wget
|
||||
|
||||
**Given** PR с фиксом
|
||||
**When** выполняется `git diff main -- Dockerfile`
|
||||
**Then** в diff нет строк `apt-get install` для пакетов `curl` или
|
||||
`wget`.
|
||||
|
||||
## AC-05. Healthcheck честно фиксирует unhealthy при отказе приложения
|
||||
|
||||
**Given** контейнер `enduro-trails-app-1` в статусе `healthy`
|
||||
**When** uvicorn останавливается внутри контейнера (`docker exec
|
||||
enduro-trails-app-1 sh -c 'kill -STOP 1'` или эквивалент через
|
||||
останов python-процесса), и приложение перестаёт отвечать на
|
||||
`http://localhost:5556/api/health`
|
||||
**Then** в течение ≤ 120 секунд статус становится `unhealthy`.
|
||||
|
||||
> Примечание: в рамках интеграционного теста допускается имитировать
|
||||
> отказ путём временной остановки контейнера-приложения и проверки,
|
||||
> что Docker фиксирует переход.
|
||||
|
||||
## AC-06. Healthcheck-команда использует stdlib python
|
||||
|
||||
**Given** YAML `docker-compose.yml`
|
||||
**When** парсится секция `app.healthcheck.test`
|
||||
**Then** первый аргумент — `"CMD"`, второй — `"python"`, третий —
|
||||
`"-c"`, четвёртый — однострочник, использующий **только** модули из
|
||||
стандартной библиотеки Python 3.12 (`urllib`, `sys`).
|
||||
|
||||
## AC-07. Внутренний таймаут запроса меньше внешнего
|
||||
|
||||
**Given** секция `healthcheck` сервиса `app`
|
||||
**When** читаются `timeout` (YAML-параметр) и `timeout=N` внутри
|
||||
`urlopen(...)`
|
||||
**Then** внутренний `timeout` строго меньше внешнего `timeout`
|
||||
(`internal < external`), чтобы python успел корректно завершиться
|
||||
и отдать exit code.
|
||||
|
||||
## AC-08. Эндпоинт /api/health не изменён
|
||||
|
||||
**Given** PR с фиксом
|
||||
**When** выполняется `git diff main -- src/api/main.py`
|
||||
**Then** в diff отсутствуют изменения функции `health()` и декоратора
|
||||
`@app.get("/api/health")` (либо они затронуты тривиально — например,
|
||||
вынос в роутер — но контракт ответа сохраняется: HTTP 200 + JSON с
|
||||
полем `status`).
|
||||
|
||||
## AC-09. CHANGELOG обновлён
|
||||
|
||||
**Given** PR с фиксом
|
||||
**When** открывается `CHANGELOG.md`
|
||||
**Then** в секции `Unreleased` (или ближайшего невыпущенного релиза)
|
||||
присутствует запись формата `fix(infra): ... healthcheck ... ET-015`.
|
||||
|
||||
## AC-10. ADR зафиксирован
|
||||
|
||||
**Given** PR с фиксом
|
||||
**When** проверяется `docs/work-items/ET-015/06-adr/`
|
||||
**Then** существует файл с ADR, описывающий решение «использовать
|
||||
python urllib вместо curl/wget» с контекстом, решением, последствиями.
|
||||
|
||||
---
|
||||
|
||||
## Критерии приёмки выполнены, если
|
||||
|
||||
Все AC-01 … AC-10 проходят. Owner вручную проверяет на mva154:
|
||||
|
||||
```bash
|
||||
ssh mva154 'docker inspect enduro-trails-app-1 \
|
||||
--format "{{.State.Health.Status}} (streak {{.State.Health.FailingStreak}})"'
|
||||
# → healthy (streak 0)
|
||||
```
|
||||
256
docs/work-items/ET-015/04-test-plan.yaml
Normal file
256
docs/work-items/ET-015/04-test-plan.yaml
Normal file
@@ -0,0 +1,256 @@
|
||||
# Test Plan: ET-015 — Healthcheck enduro-trails-app
|
||||
#
|
||||
# Базовые документы:
|
||||
# - 01-brd.md
|
||||
# - 02-trz.md
|
||||
# - 03-acceptance-criteria.md
|
||||
#
|
||||
# Категории тестов:
|
||||
# unit — изолированный, без Docker
|
||||
# integration — с реальным docker compose
|
||||
# e2e — на тестовой среде mva154
|
||||
# static — статический анализ файлов в репо
|
||||
|
||||
work_item: ET-015
|
||||
version: "1.0"
|
||||
date: "2026-06-05"
|
||||
|
||||
tests:
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# STATIC
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
- id: ST-01
|
||||
name: "docker-compose.yml не содержит curl в healthcheck"
|
||||
type: static
|
||||
covers: [AC-03]
|
||||
given: "Ветка feature/ET-015-... в рабочем дереве"
|
||||
steps:
|
||||
- "grep -nE '\\bcurl\\b' docker-compose.yml || true"
|
||||
expected:
|
||||
- "В выводе нет строк, попадающих в секцию app.healthcheck."
|
||||
automatable: true
|
||||
tooling: "make lint / отдельная проверка в scripts/"
|
||||
|
||||
- id: ST-02
|
||||
name: "Dockerfile не устанавливает curl/wget"
|
||||
type: static
|
||||
covers: [AC-04]
|
||||
given: "Текущий Dockerfile"
|
||||
steps:
|
||||
- "grep -nE 'apt-get +install.*\\b(curl|wget)\\b' Dockerfile || true"
|
||||
expected:
|
||||
- "Совпадений нет."
|
||||
automatable: true
|
||||
|
||||
- id: ST-03
|
||||
name: "Healthcheck использует python и stdlib"
|
||||
type: static
|
||||
covers: [AC-06]
|
||||
given: "YAML docker-compose.yml"
|
||||
steps:
|
||||
- "Распарсить YAML (python -c 'import yaml,sys; print(yaml.safe_load(open(\"docker-compose.yml\"))[\"services\"][\"app\"][\"healthcheck\"][\"test\"])')"
|
||||
expected:
|
||||
- "Массив начинается с ['CMD', 'python', '-c', ...]."
|
||||
- "Четвёртый элемент содержит 'urllib.request' и 'sys.exit'."
|
||||
- "Не импортируются сторонние пакеты (нет 'requests', 'httpx', и т.п.)."
|
||||
automatable: true
|
||||
|
||||
- id: ST-04
|
||||
name: "Внутренний timeout urlopen меньше внешнего timeout healthcheck"
|
||||
type: static
|
||||
covers: [AC-07]
|
||||
given: "Парсенный healthcheck"
|
||||
steps:
|
||||
- "Извлечь N из 'timeout=N' внутри строки python -c."
|
||||
- "Извлечь M из YAML-поля healthcheck.timeout (например, '5s' → 5)."
|
||||
expected:
|
||||
- "N < M (по умолчанию 3 < 5)."
|
||||
automatable: true
|
||||
|
||||
- id: ST-05
|
||||
name: "Эндпоинт /api/health не сломан изменениями"
|
||||
type: static
|
||||
covers: [AC-08]
|
||||
given: "PR с фиксом против main"
|
||||
steps:
|
||||
- "git diff main -- src/api/main.py | grep -E '^[+-].*(api/health|async def health)' || true"
|
||||
expected:
|
||||
- "Либо нет изменений, либо рефакторинг без слома контракта."
|
||||
automatable: true
|
||||
|
||||
- id: ST-06
|
||||
name: "CHANGELOG обновлён"
|
||||
type: static
|
||||
covers: [AC-09]
|
||||
given: "CHANGELOG.md"
|
||||
steps:
|
||||
- "grep -nE 'ET-015' CHANGELOG.md"
|
||||
expected:
|
||||
- "Минимум одна строка с упоминанием ET-015 в Unreleased/ближайшей версии."
|
||||
automatable: true
|
||||
|
||||
- id: ST-07
|
||||
name: "ADR существует"
|
||||
type: static
|
||||
covers: [AC-10]
|
||||
given: "docs/work-items/ET-015/06-adr/"
|
||||
steps:
|
||||
- "ls docs/work-items/ET-015/06-adr/*.md"
|
||||
expected:
|
||||
- "Минимум один .md-файл с описанием решения healthcheck-via-python."
|
||||
automatable: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# UNIT
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
- id: UT-01
|
||||
name: "Python one-liner возвращает 0 при HTTP 200"
|
||||
type: unit
|
||||
covers: [AC-06]
|
||||
given: "Запущен локально `make dev` (uvicorn на :5556) или мок-сервер"
|
||||
steps:
|
||||
- "Скопировать строку python -c '...' из healthcheck."
|
||||
- "Запустить `python -c '...'` на хосте против http://localhost:5556/api/health."
|
||||
- "Проверить $? == 0."
|
||||
expected:
|
||||
- "exit code = 0."
|
||||
automatable: true
|
||||
|
||||
- id: UT-02
|
||||
name: "Python one-liner возвращает не 0 при недоступном порту"
|
||||
type: unit
|
||||
covers: [AC-05, AC-06]
|
||||
given: "Никто не слушает :5556 (uvicorn остановлен)"
|
||||
steps:
|
||||
- "Запустить ту же команду python -c '...'"
|
||||
- "Проверить exit code."
|
||||
expected:
|
||||
- "exit code != 0 (URLError → ненулевой код)."
|
||||
automatable: true
|
||||
|
||||
- id: UT-03
|
||||
name: "Python one-liner возвращает не 0 при HTTP 500"
|
||||
type: unit
|
||||
covers: [AC-06]
|
||||
given: "Мок-сервер на :5556, отдающий 500 на /api/health"
|
||||
steps:
|
||||
- "Запустить python one-liner."
|
||||
expected:
|
||||
- "exit code != 0 (HTTPError или sys.exit(1))."
|
||||
automatable: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# INTEGRATION
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
- id: IT-01
|
||||
name: "docker compose up: контейнер становится healthy за ≤ 120s"
|
||||
type: integration
|
||||
covers: [AC-01]
|
||||
given: "Чистая локальная машина с Docker и доступом к данным /home/slin/enduro-trails/data"
|
||||
steps:
|
||||
- "docker compose down -v"
|
||||
- "docker compose up -d app"
|
||||
- "Запустить цикл: `while true; do status=$(docker inspect $(docker compose ps -q app) --format '{{.State.Health.Status}}'); echo $status; [ \"$status\" = \"healthy\" ] && break; sleep 5; done` с таймаутом 120s"
|
||||
expected:
|
||||
- "Статус становится healthy в течение 120 секунд."
|
||||
- "FailingStreak == 0 после перехода."
|
||||
automatable: true
|
||||
|
||||
- id: IT-02
|
||||
name: "Healthy остаётся стабильным 5 минут"
|
||||
type: integration
|
||||
covers: [AC-02]
|
||||
given: "Контейнер в статусе healthy"
|
||||
steps:
|
||||
- "Подождать 5 минут (10 циклов healthcheck при interval=30s)."
|
||||
- "docker inspect ... --format '{{.State.Health.Status}}'"
|
||||
- "docker inspect ... --format '{{json .State.Health.Log}}' | jq '.[-5:] | map(.ExitCode)'"
|
||||
expected:
|
||||
- "Статус == healthy."
|
||||
- "Все 5 последних ExitCode == 0."
|
||||
|
||||
- id: IT-03
|
||||
name: "Переход в unhealthy при остановке приложения"
|
||||
type: integration
|
||||
covers: [AC-05]
|
||||
given: "Контейнер healthy"
|
||||
steps:
|
||||
- "docker exec <container> sh -c 'pkill -STOP -f uvicorn' (или эквивалент: остановить главный процесс)"
|
||||
- "Ждать до 120 секунд."
|
||||
- "docker inspect ... --format '{{.State.Health.Status}}'"
|
||||
expected:
|
||||
- "Статус становится unhealthy в течение 120 секунд."
|
||||
- "FailingStreak >= retries (>= 3)."
|
||||
teardown:
|
||||
- "docker compose restart app — вернуть в рабочее состояние."
|
||||
|
||||
- id: IT-04
|
||||
name: "Healthcheck не требует ребилда образа"
|
||||
type: integration
|
||||
covers: [AC-04]
|
||||
given: "Старый образ (с поломанным curl-healthcheck) уже собран локально"
|
||||
steps:
|
||||
- "Применить новый docker-compose.yml без `docker compose build`."
|
||||
- "docker compose up -d app (только пересоздание контейнера)."
|
||||
- "Подождать до 120 секунд."
|
||||
expected:
|
||||
- "Контейнер healthy без пересборки образа."
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# E2E (на mva154)
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
- id: E2E-01
|
||||
name: "После make deploy-test контейнер healthy на mva154"
|
||||
type: e2e
|
||||
covers: [AC-01, AC-02]
|
||||
given: "Ветка смерджена в main, CI прошёл, выполнен make deploy-test"
|
||||
steps:
|
||||
- "ssh mva154 'docker inspect enduro-trails-app-1 --format \"{{.State.Health.Status}}\"'"
|
||||
- "Повторить через 5 и 10 минут."
|
||||
expected:
|
||||
- "Все три замера: healthy."
|
||||
- "FailingStreak == 0."
|
||||
automatable: false
|
||||
owner: "ops"
|
||||
|
||||
- id: E2E-02
|
||||
name: "Приложение продолжает отвечать снаружи"
|
||||
type: e2e
|
||||
covers: [AC-08]
|
||||
given: "Контейнер healthy на mva154"
|
||||
steps:
|
||||
- "curl -sS -o /dev/null -w '%{http_code} %{time_total}\\n' https://openclaw.mva154.duckdns.org/enduro/api/health"
|
||||
expected:
|
||||
- "HTTP 200, time_total < 1s."
|
||||
automatable: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Покрытие критериев приёмки
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
coverage_matrix:
|
||||
AC-01: [IT-01, E2E-01]
|
||||
AC-02: [IT-02, E2E-01]
|
||||
AC-03: [ST-01]
|
||||
AC-04: [ST-02, IT-04]
|
||||
AC-05: [UT-02, IT-03]
|
||||
AC-06: [ST-03, UT-01, UT-03]
|
||||
AC-07: [ST-04]
|
||||
AC-08: [ST-05, E2E-02]
|
||||
AC-09: [ST-06]
|
||||
AC-10: [ST-07]
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Definition of Done для тестирования
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
done_when:
|
||||
- "Все ST-* и UT-* проходят в make test / CI."
|
||||
- "IT-01, IT-02, IT-03, IT-04 пройдены вручную или в integration-CI."
|
||||
- "E2E-01 подтверждён ops после deploy-test."
|
||||
- "E2E-02 возвращает HTTP 200."
|
||||
Reference in New Issue
Block a user