From c2cf8280cac40848be4303f8f7e5283f3e0be8b9 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Fri, 5 Jun 2026 15:11:28 +0000 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=101 --- docs/work-items/ET-015/01-brd.md | 105 +++++++ docs/work-items/ET-015/02-trz.md | 169 ++++++++++++ .../ET-015/03-acceptance-criteria.md | 111 ++++++++ docs/work-items/ET-015/04-test-plan.yaml | 256 ++++++++++++++++++ 4 files changed, 641 insertions(+) create mode 100644 docs/work-items/ET-015/01-brd.md create mode 100644 docs/work-items/ET-015/02-trz.md create mode 100644 docs/work-items/ET-015/03-acceptance-criteria.md create mode 100644 docs/work-items/ET-015/04-test-plan.yaml diff --git a/docs/work-items/ET-015/01-brd.md b/docs/work-items/ET-015/01-brd.md new file mode 100644 index 0000000..7ed73a6 --- /dev/null +++ b/docs/work-items/ET-015/01-brd.md @@ -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. diff --git a/docs/work-items/ET-015/02-trz.md b/docs/work-items/ET-015/02-trz.md new file mode 100644 index 0000000..f0c94ea --- /dev/null +++ b/docs/work-items/ET-015/02-trz.md @@ -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` доступны из ветки. diff --git a/docs/work-items/ET-015/03-acceptance-criteria.md b/docs/work-items/ET-015/03-acceptance-criteria.md new file mode 100644 index 0000000..73c6a22 --- /dev/null +++ b/docs/work-items/ET-015/03-acceptance-criteria.md @@ -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) +``` diff --git a/docs/work-items/ET-015/04-test-plan.yaml b/docs/work-items/ET-015/04-test-plan.yaml new file mode 100644 index 0000000..fd2a2f7 --- /dev/null +++ b/docs/work-items/ET-015/04-test-plan.yaml @@ -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 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."