---
type: test-plan
work_item_id: ET-013
title: "Test Plan: Перепады высот на z9-z11"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
  - "agent:analyst"
related:
  - "PH-6.terrain"
  - "ET-007"

scope_note: >
  ET-013 — frontend-калибровка: понижает UI-минзум hillshade с 10 до 9
  и переводит paint-параметры (raster-opacity, raster-contrast,
  raster-resampling) hillshade и TRI в zoom-aware форму. Backend
  и pipeline растровых тайлов не трогаются. Тест-план фокусируется
  на:
    (1) корректности новых zoom-tier paint-выражений;
    (2) обратной совместимости applyTerrainLayer;
    (3) визуальной читаемости перепадов на z9-z11;
    (4) регрессии z8 (TRI не изменился), z14 (hillshade не перегрет);
    (5) совместимости с тёмной темой и спутниковой подложкой;
    (6) что network-объём не уплыл больше +35%.

test_suites:

  - name: unit-terrain-paint
    type: unit
    description: "Структура paint-выражений HILLSHADE_PAINT и TRI_PAINT"
    cases:
      - id: UT-PAINT-HS-OPACITY
        name: "HILLSHADE_PAINT: raster-opacity — interpolate с правильными stops"
        input: |
          Python-парсер: чтение src/web/app.js, regex по блоку
          HILLSHADE_PAINT = { ... }; вытаскивание raster-opacity.
        expected: |
          Тип: ['interpolate', ['linear'], ['zoom'], ...].
          Stops содержат: (9, 0.65), (10, 0.60), (11, 0.55),
          (12, 0.50), (14, 0.40). Допустимо отклонение значений ±0.05
          (калибровка) — но порядок монотонно убывающий от 9 к 14.

      - id: UT-PAINT-HS-CONTRAST
        name: "HILLSHADE_PAINT: raster-contrast — пик на z9, 0 на z14"
        input: |
          Тот же парсер.
        expected: |
          Тип interpolate. Значение на z=9 ≥ 0.30. Значение на z=14
          ≤ 0.10. Монотонно убывает.

      - id: UT-PAINT-HS-RESAMPLING
        name: "HILLSHADE_PAINT: raster-resampling = 'nearest'"
        input: |
          Парсер.
        expected: |
          Строка 'nearest' (не 'linear').

      - id: UT-PAINT-TRI-OPACITY-Z8
        name: "TRI_PAINT: на z8 opacity = 0.70 (регрессия)"
        input: |
          Парсер по TRI_PAINT.
        expected: |
          Stop (8, 0.70) присутствует ровно (без округления).

      - id: UT-PAINT-TRI-OPACITY-PEAK
        name: "TRI_PAINT: пик на z9-z11"
        input: |
          Парсер.
        expected: |
          Stops содержат (10, X) с X ≥ 0.80 и (11, Y) с Y ≥ 0.80.

      - id: UT-PAINT-TRI-RESAMPLING
        name: "TRI_PAINT: raster-resampling = 'nearest'"
        input: |
          Парсер.
        expected: |
          'nearest'.

      - id: UT-PAINT-COMPAT-01
        name: "applyTerrainLayer обратно-совместим с числовым opacity"
        input: |
          Вызов с opacity=0.5 (Node + JSDOM-mock карты).
        expected: |
          Внутри map.addLayer передан paint:
          { 'raster-opacity': 0.5, 'raster-resampling': 'linear' }.
        notes: |
          Если запуск JS-теста не настроен — заменить на статический
          grep по src/web/app.js: проверить ветвление
          'typeof opacityOrPaint === "number"'.

      - id: UT-PAINT-COMPAT-02
        name: "applyTerrainLayer принимает paint-объект"
        input: |
          Вызов с opacityOrPaint = { 'raster-opacity': 0.4,
          'raster-contrast': 0.2, 'raster-resampling': 'nearest' }.
        expected: |
          Этот объект передан в map.addLayer paint как есть.

      - id: UT-REG-MINZOOM-9
        name: "updateHillshadeAvailability порог = 9"
        input: |
          grep по src/web/app.js: 'if (zoom < 9)' внутри функции
          updateHillshadeAvailability.
        expected: |
          Совпадение найдено; 'if (zoom < 10)' отсутствует.

      - id: UT-REG-HINT-TEXT
        name: "Hint текст обновлён до 'Зум 9+'"
        input: |
          grep по src/web/index.html: '#terrain-hillshade-hint'
          содержит 'Зум 9+'.
        expected: |
          Совпадение найдено; 'Зум 10+' отсутствует.

      - id: UT-REG-CALLERS
        name: "applyTerrainLayer вызывается ровно дважды в onTerrainCheckbox"
        input: |
          regex 'applyTerrainLayer\s*\(' в src/web/*.js — count.
        expected: |
          Минимум 2 вызова в src/web/app.js. Все они находятся
          внутри функции onTerrainCheckbox.

  - name: integration-terrain-tiles
    type: integration
    description: "Endpoint /terrain/{layer}/{z}/{x}/{y}.png на z9-z11"
    cases:
      - id: IT-TILE-Z9-01
        name: "Тайл z=9 для hillshade: 200 или skipped если данных нет"
        input: |
          Test-среда или CI с TERRAIN_DIR. Найти первый существующий
          тайл z9 в директории hillshade, выполнить GET.
        expected: |
          Если data/terrain/hillshade/9/ существует:
            status 200, content-type image/png, тело > 0.
          Иначе:
            test skipped с reason 'PH-6 data not in repo'.

      - id: IT-TILE-Z10-01
        name: "Тайл z=10 для hillshade: 200 или skipped"
        input: |
          То же, что IT-TILE-Z9-01 для z=10.
        expected: |
          status 200 или skipped.

      - id: IT-TILE-Z11-01
        name: "Тайл z=11 для hillshade: 200 или skipped"
        input: |
          То же для z=11.
        expected: |
          status 200 или skipped.

      - id: IT-TILE-TRI-Z9
        name: "TRI на z9 доступен (минзум 5, тайлы должны быть)"
        input: |
          GET tiles/9/X/Y.png под TRI.
        expected: |
          200 или skipped (если данных нет на CI).

      - id: IT-TILE-INVALID-LAYER
        name: "Неизвестный layer → 404 (регрессия)"
        input: |
          GET /terrain/unknown/9/0/0.png
        expected: |
          status 404.

      - id: IT-TILE-MISSING
        name: "Несуществующий тайл → 404 (регрессия)"
        input: |
          GET /terrain/hillshade/9/99999/99999.png
        expected: |
          status 404.

      - id: IT-TILE-CACHE-HEADER
        name: "Cache-Control: immutable сохраняется"
        input: |
          GET существующего тайла.
        expected: |
          Header 'Cache-Control' содержит 'immutable' и max-age=31536000.

  - name: regression-existing
    type: regression
    description: "Регрессия ET-007 / PH-6 / общих unit-тестов"
    cases:
      - id: RG-UNIT-ALL
        name: "Все unit-тесты проекта зелёные"
        input: "pytest tests/unit/ -v"
        expected: "exit-code 0"

      - id: RG-INTEG-ALL
        name: "Все integration-тесты проекта зелёные"
        input: "pytest tests/integration/ -v"
        expected: "exit-code 0"

      - id: RG-LINT
        name: "Линтеры зелёные"
        input: "make lint"
        expected: "exit-code 0"

  - name: ui-playwright
    type: ui
    description: "Playwright UI-тесты на test-среде"
    reference: "04b-ui-test-cases.md"
    cases:
      - id: UI-LINK-01
        name: "См. 04b-ui-test-cases.md — TC-UI-01..TC-UI-12"
        expected: |
          Каждый TC выполняется; check-visual подтверждается
          оператором либо визуальным diff-инструментом
          (baseline до ET-013 vs текущий).

  - name: manual-deploy-validation
    type: e2e
    description: "Ручная проверка в test-среде после деплоя"
    marker: "manual"
    cases:
      - id: E2E-PRE-DEPLOY-01
        name: "Pre-deploy: тайлы z9-z11 на test-среде доступны"
        steps:
          - "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/9/308/158.png | head -1"
          - "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/10/617/317.png | head -1"
          - "curl -sI https://openclaw.mva154.duckdns.org/enduro/terrain/hillshade/11/1234/635.png | head -1"
          - "Все три — HTTP/1.1 200 OK. При 404 — стоп, открыть PH-6 follow-up."
          - "Зафиксировать в 14-deploy-log.md."

      - id: E2E-DEPLOY-01
        name: "Hillshade доступен на z=9"
        steps:
          - "Открыть https://openclaw.mva154.duckdns.org/enduro/"
          - "localStorage.clear(); location.reload()"
          - "Click #terrain-toggle"
          - "В Console: window._map.setZoom(9); window._map.setCenter([37.6, 54.5])"
          - "Wait 2s"
          - "Кнопка #terrain-hillshade-cb имеет disabled=false"
          - "Hint #terrain-hillshade-hint имеет display:none"
          - "Click #terrain-hillshade-cb"
          - "Wait 3s"
          - "На карте видны тени"
          - "Screenshot et013-deploy-z9.png"
          - "Зафиксировать в 14-deploy-log.md"

      - id: E2E-DEPLOY-02
        name: "Network-объём: рост ≤ 35%"
        steps:
          - "Открыть DevTools Network, фильтр /terrain/"
          - "Очистить network log"
          - "В Console: window._map.setZoom(8); ждать 3s; setZoom(9); ждать 3s; setZoom(10); ждать 3s; setZoom(11); ждать 3s"
          - "Замерить суммарный transferred size в фильтре /terrain/"
          - "Сравнить с baseline (записан в 13-test-report.md до ET-013): рост ≤ 135%"
          - "Зафиксировать"

      - id: E2E-DEPLOY-03
        name: "Регрессия z=8 (TRI выглядит как до ET-013)"
        steps:
          - "localStorage.clear(); location.reload()"
          - "Включить только #terrain-tri-cb (без hillshade)"
          - "window._map.setZoom(8); setCenter([37.6, 54.5])"
          - "Screenshot et013-deploy-z8-tri-regress.png"
          - "Визуально сравнить с baseline из 13-test-report.md до ET-013 — не отличается заметно."

      - id: E2E-DEPLOY-04
        name: "Регрессия z=14 (hillshade не перегрет)"
        steps:
          - "Включить #terrain-hillshade-cb"
          - "window._map.setZoom(14); setCenter([37.6, 54.5])"
          - "Screenshot et013-deploy-z14-regress.png"
          - "Эффективное raster-opacity ≈ 0.40, raster-contrast ≈ 0"
          - "В Console: window._map.getPaintProperty('terrain-hillshade', 'raster-opacity')"
          - "(вернёт interpolate-выражение — proof zoom-aware)"

      - id: E2E-DEPLOY-05
        name: "Спутник + hillshade на z=10 (R-3)"
        steps:
          - "Включить hillshade, переключить #base-btn-satellite"
          - "window._map.setZoom(10); setCenter([37.6, 54.5])"
          - "Screenshot et013-deploy-z10-sat.png"
          - "Визуальная приёмка: hillshade видим, не глушит снимок"
          - "При проблеме — задача отправляется на корректировку stops"

      - id: E2E-DEPLOY-06
        name: "Тёмная тема + hillshade на z=10 (R-2)"
        steps:
          - "Click #btn-theme (переключить в тёмную)"
          - "window._map.setZoom(10)"
          - "Screenshot et013-deploy-z10-dark.png"
          - "Визуальная приёмка: hillshade читается, не сливается с тёмной подложкой"

      - id: E2E-DEPLOY-07
        name: "Persistence: F5 не теряет состояние"
        steps:
          - "Включить оба чекбокса"
          - "location.reload()"
          - "Чекбоксы остаются включёнными"
          - "На текущем zoom оба слоя восстановлены"

test_data:
  fixtures_dir: "tests/fixtures/terrain/"
  fixtures:
    - name: "hillshade-z9-sample.png"
      description: |
        Опционально: один валидный PNG-тайл из data/terrain/hillshade/9/
        для CI-окружения без полного набора данных. Скопировать любой
        тайл над ЦФО, переименовать. ~10 KB.
    - name: "hillshade-z10-sample.png"
      description: "То же для z10."
    - name: "tri-z10-sample.png"
      description: "TRI sample для z10."
  notes:
    - "Если на CI нет TERRAIN_DIR с данными — IT-TILE-* тесты skipped (REQ-F-15)."
    - "Сравнения 'до/после' визуальные — baseline скриншоты лежат в 13-test-report.md и фиксируются до начала ET-013."
    - "Для unit-тестов paint никаких fixture не нужно — парсинг исходника."

test_environment:
  unit:
    - "Python 3.12, pytest"
    - "regex-парсер src/web/app.js (Вариант B в TRZ REQ-F-13)"
    - "Опционально Node + JSDOM, если в проекте появятся JS-тесты"
  integration:
    - "FastAPI TestClient против src.api.main:app"
    - "TERRAIN_DIR через env или skip-if-missing"
  performance:
    - "Не требуется специально: NFR-01/02 говорят о невидимом изменении render-time"
    - "Сетевой объём — ручной замер в DevTools Network (E2E-DEPLOY-02)"
  e2e:
    - "Test-среда https://openclaw.mva154.duckdns.org/enduro/"
    - "Playwright (см. 04b-ui-test-cases.md)"

ci_gates:
  - "Unit UT-PAINT-* и UT-REG-* — обязательны (AC-15)"
  - "Integration IT-TILE-* — обязательны (с skipif для отсутствующих данных) (AC-16)"
  - "Регрессия RG-UNIT-ALL, RG-INTEG-ALL, RG-LINT — обязательны (AC-17, AC-18)"
  - "Pre-deploy E2E-PRE-DEPLOY-01 — ручной gate перед merge (AC-19)"
  - "UI-тесты Playwright — после деплоя, фиксация в 13-test-report.md"
  - "E2E-DEPLOY-01..07 — ручные шаги в 14-deploy-log.md"
---
