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