# 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."