---
type: test-plan
work_item_id: ET-012
title: "Test Plan: Показывать пользовательские треки с зума z5"
version: 1
status: draft
created_at: 2026-06-04
updated_at: 2026-06-04
authors:
  - "agent:analyst"
related:
  - "ET-008"
  - "ET-009"
  - "ET-011"

scope_note: >
  ET-012 опускает порог видимости слоя публичных GPS-треков с z8 до z5.
  Изменения локализованы:
    - backend mvt.py: zoom-tier для z5/z6 (min_length, limit, tolerance);
    - frontend gps_tracks.js: константа GPS_TRACKS_MIN_ZOOM=5,
      line-width stops для z5 в основном слое и halo;
    - index.html: текст hint «Зум 5+».
  Тест-план фокусируется на:
    (1) корректности новых zoom-tier'ов в build_gps_mvt и _simplify_coords;
    (2) что endpoint отдаёт нормально-размерные MVT на z5-z7;
    (3) что клиент действительно показывает слой на z5-z7;
    (4) что регрессий ET-008/009/011 нет;
    (5) что производительность не уплыла.

test_suites:

  - name: unit-mvt-zoom-tiers
    type: unit
    description: "Тиры min_length_m и limit в build_gps_mvt по зумам"
    cases:
      - id: UT-Z5-01
        name: "z=5: треки < 10 км отфильтровываются"
        input: |
          Mock rows: 10 треков, длина = [500, 2000, 3000, 8000, 12000, 15000, 25000, 50000, 80000, 120000].
          Вызов build_gps_mvt(rows, z=5, x=19, y=9).
        expected: |
          В MVT попадают только треки длиной >= 10000 м, т.е. ровно 6 features.

      - id: UT-Z5-02
        name: "z=5: limit=1500"
        input: |
          Mock rows: 2000 треков длиной 15 км каждый (все пройдут min_length).
        expected: |
          В MVT попадают первые 1500 features, остальные отбрасываются.

      - id: UT-Z6-01
        name: "z=6: треки < 5 км отфильтровываются"
        input: |
          Mock rows: 5 треков, длина = [1000, 3000, 5000, 7000, 10000].
        expected: |
          В MVT 3 features (5000, 7000, 10000).

      - id: UT-Z6-02
        name: "z=6: limit=2000"
        input: |
          Mock rows: 2500 треков длиной 6 км каждый.
        expected: |
          В MVT 2000 features.

      - id: UT-Z7-01
        name: "z=7: регрессия — поведение до ET-012"
        input: |
          Mock rows: 4 трека [1000, 2000, 3000, 5000].
        expected: |
          В MVT 3 features (2000, 3000, 5000), как раньше.

      - id: UT-Z8-01
        name: "z=8: регрессия — нет min_length-фильтра"
        input: |
          Mock rows: 4 трека [500, 1000, 2000, 5000].
        expected: |
          В MVT 4 features, limit=8000.

      - id: UT-Z12-01
        name: "z=12: регрессия — limit=25000, без min_length"
        input: |
          Mock rows: 100 треков любой длины.
        expected: |
          В MVT 100 features.

  - name: unit-mvt-simplify
    type: unit
    description: "Tolerance Douglas-Peucker по зумам в _simplify_coords"
    cases:
      - id: UT-SIMP-Z5-01
        name: "z=5: прямая линия 100 точек → ≤ 5 точек"
        input: |
          coords = [(37.0 + i*0.001, 55.0 + i*0.001) for i in range(100)]
          (приблизительно прямая ~10 км по диагонали)
        expected: |
          len(_simplify_coords(coords, 5)) <= 5

      - id: UT-SIMP-Z5-02
        name: "z=5: зигзаг с амплитудой < tolerance → 2 точки"
        input: |
          coords = зигзаг 100 точек, амплитуда 0.01° (~1 км)
        expected: |
          len(_simplify_coords(coords, 5)) == 2 (только концы)

      - id: UT-SIMP-Z6-01
        name: "z=6: зигзаг 5 км → видны крупные пики"
        input: |
          coords = зигзаг 100 точек, амплитуда 0.05° (~5 км)
        expected: |
          len(_simplify_coords(coords, 6)) > 5

      - id: UT-SIMP-Z7-01
        name: "z=7: регрессия — tolerance = 0.008"
        input: |
          coords = зигзаг 100 точек, амплитуда 0.005° (~500 м)
        expected: |
          len(_simplify_coords(coords, 7)) близок к до-ET-012 значению
          (округлённо в пределах +/-1).

      - id: UT-SIMP-Z10-01
        name: "z=10: регрессия — tolerance = 0.0005"
        input: |
          coords = зигзаг 100 точек, амплитуда 0.001° (~100 м)
        expected: |
          Поведение совпадает с до-ET-012 (контрольный snapshot).

      - id: UT-SIMP-Z12-01
        name: "z=12: регрессия — без упрощения"
        input: |
          coords = 100 точек
        expected: |
          _simplify_coords(coords, 12) is coords (или эквивалент)

      - id: UT-SIMP-EDGE-01
        name: "Слишком мало точек → возвращаем как есть"
        input: |
          coords = [(37.0, 55.0), (37.001, 55.001)] (2 точки)
        expected: |
          На любом zoom — функция возвращает [(37.0, 55.0), (37.001, 55.001)].

      - id: UT-SIMP-EDGE-02
        name: "DP схлопнул до < 2 точек → возвращаем оригинал"
        input: |
          coords = 100 одинаковых точек (вырожденный трек)
        expected: |
          Функция возвращает оригинальный coords, не пустой список.

  - name: integration-tile-endpoint
    type: integration
    description: "Endpoint /api/gps-tracks/tiles/{z}/{x}/{y}.mvt на z=5..7"
    cases:
      - id: IT-Z5-01
        name: "Тайл z=5 над Москвой: 200, тело > 0, < 200 KB"
        input: |
          Test DB: 50 треков по ЦФО, длина 12..30 км каждый.
          GET /api/gps-tracks/tiles/5/19/9.mvt
        expected: |
          status 200,
          Content-Type 'application/x-protobuf',
          0 < len(body) < 200_000

      - id: IT-Z5-02
        name: "Тайл z=5 с большой БД: limit держит размер"
        input: |
          Test DB: 200 треков по ЦФО, длина 12..30 км.
          GET /api/gps-tracks/tiles/5/19/9.mvt
        expected: |
          status 200,
          len(body) < 200_000,
          mapbox_vector_tile.decode(body)['gps_tracks']['features'] <= 1500

      - id: IT-Z5-03
        name: "Тайл z=5 над пустым регионом: пустое тело"
        input: |
          Test DB: те же 50 треков по ЦФО.
          GET /api/gps-tracks/tiles/5/4/12.mvt (Тихий океан)
        expected: |
          status 200,
          len(body) == 0

      - id: IT-Z6-01
        name: "Тайл z=6 над Москвой: больше фич, чем z=5"
        input: |
          Test DB: 100 треков, длина 4..20 км.
          GET /api/gps-tracks/tiles/6/38/19.mvt
        expected: |
          status 200,
          features_count(z=6) >= features_count(z=5) для того же региона,
          len(body) < 200_000

      - id: IT-Z7-01
        name: "Тайл z=7 над Москвой: регрессия + плюс короткие треки"
        input: |
          GET /api/gps-tracks/tiles/7/77/39.mvt с теми же 100 треками.
        expected: |
          status 200,
          features_count(z=7) >= features_count(z=6),
          features_count(z=7) <= 3000

      - id: IT-CACHE-01
        name: "LRU-кэш: второй запрос — X-Cache: HIT"
        input: |
          GET /api/gps-tracks/tiles/5/19/9.mvt дважды подряд.
        expected: |
          1-й ответ: header X-Cache: MISS.
          2-й ответ: header X-Cache: HIT, тело идентично 1-му.

      - id: IT-CACHE-02
        name: "Сброс кэша через /cache/clear"
        input: |
          GET tiles/5/19/9.mvt → POST /api/gps-tracks/cache/clear → GET tiles/5/19/9.mvt
        expected: |
          1-й ответ MISS, 2-й (после clear) MISS.

      - id: IT-REGRESS-Z8-01
        name: "Регрессия z=8: контракт MVT не изменился"
        input: |
          GET /api/gps-tracks/tiles/8/154/79.mvt на тестовой БД.
          (Тайл-координаты выбраны над Москвой.)
        expected: |
          features_count(z=8) точно совпадает с snapshot до ET-012
          (записывается в tests/fixtures/gps-tracks/mvt-z8-snapshot.json).

      - id: IT-REGRESS-Z10-01
        name: "Регрессия z=10"
        input: |
          GET /api/gps-tracks/tiles/10/617/319.mvt
        expected: |
          features_count(z=10) совпадает с snapshot до ET-012.

      - id: IT-VALID-01
        name: "z вне диапазона — 400"
        input: |
          GET /api/gps-tracks/tiles/-1/0/0.mvt и tiles/23/0/0.mvt
        expected: |
          status 400, detail 'Invalid z'

  - name: integration-api-geojson-cutoff
    type: integration
    description: "GeoJSON-слой не изменился"
    cases:
      - id: IT-GEO-01
        name: "GET /api/gps-tracks?bbox=… работает как раньше"
        input: |
          GET /api/gps-tracks?bbox=37,55,38,56&limit=500
        expected: |
          status 200,
          FeatureCollection с features, total_in_bbox, returned, truncated —
          контракт идентичен ET-009.

  - name: performance
    type: performance
    description: "Производительность build_gps_mvt на z=5"
    marker: "@pytest.mark.perf"
    cases:
      - id: PERF-Z5-01
        name: "build_gps_mvt на z=5 при 500 треках"
        input: |
          Test DB: 500 треков длиной 12-25 км по ЦФО.
          10 повторных вызовов build_gps_mvt(rows, 5, 19, 9).
        expected: |
          avg time <= 200 ms,
          p95 time <= 500 ms на CI-runner (метрика M-6).

      - id: PERF-Z5-02
        name: "build_gps_mvt на z=5 при 5000 треках (стресс)"
        input: |
          Test DB: 5000 треков, разные длины.
          5 повторных вызовов.
        expected: |
          p95 time <= 1500 ms.

      - id: PERF-ENDPOINT-01
        name: "Endpoint p95 на z=5 (cold)"
        input: |
          10 cold-запросов tile-endpoint (после cache clear) на test-БД.
        expected: |
          p95 <= 700 ms.

      - id: PERF-ENDPOINT-02
        name: "Endpoint p95 на z=5 (hot, кэш)"
        input: |
          100 повторных запросов одного тайла после прогрева.
        expected: |
          p95 <= 50 ms.

  - name: regression-existing
    type: regression
    description: "Регрессия ET-008 / ET-009 / ET-011"
    cases:
      - id: RG-08-01
        name: "Все unit-тесты ET-008 проходят"
        input: "pytest tests/unit/test_gps_*.py -v (за исключением новых ET-012)"
        expected: "exit-code 0"

      - id: RG-09-01
        name: "Все unit-тесты ET-009 (parser EnduroRussia/Wikiloc)"
        input: "pytest tests/unit/test_gps_tracks_enduro_russia.py tests/unit/test_gps_tracks_wikiloc.py -v"
        expected: "exit-code 0"

      - id: RG-11-01
        name: "Тесты ET-011 download GPX"
        input: "pytest tests/integration/test_gps_download.py -v"
        expected: "exit-code 0"

      - id: RG-INT-01
        name: "Все integration-тесты"
        input: "pytest tests/integration/ -v"
        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-Z5..TC-UI-12-Z5-Q"
        expected: "Каждый TC выполняется и check-visual подтверждается оператором."

  - name: manual-deploy-validation
    type: e2e
    description: "Ручная проверка в test-среде после деплоя"
    marker: "manual"
    cases:
      - id: E2E-DEPLOY-01
        name: "Включить слой и поставить zoom=5"
        steps:
          - "Открыть https://openclaw.mva154.duckdns.org/enduro/"
          - "Open DevTools, в Console: localStorage.clear() для чистого старта"
          - "Click #terrain-toggle"
          - "Click #public-tracks-cb (включить)"
          - "В Console: window._map.setZoom(5); window._map.setCenter([37.6, 55.7])"
          - "Ждать 3 секунды"
          - "Visual: видны линии публичных треков"
          - "Зафиксировать скриншот в 14-deploy-log.md"

      - id: E2E-DEPLOY-02
        name: "Network: размер тайла z=5"
        steps:
          - "В DevTools Network отфильтровать по 'tiles/5'"
          - "Проверить: каждый ответ ≤ 200 KB (Size column)"
          - "Зафиксировать в 14-deploy-log.md"

      - id: E2E-DEPLOY-03
        name: "Уменьшить зум до z=4 — hint показывается"
        steps:
          - "window._map.setZoom(4)"
          - "Visual: hint 'Зум 5+' появился"
          - "На карте нет линий публичных треков"

      - id: E2E-DEPLOY-04
        name: "Зум z=12 — переход на GeoJSON"
        steps:
          - "window._map.setZoom(12)"
          - "Wait 1.5s"
          - "В DevTools Network отфильтровать по '/api/gps-tracks?bbox'"
          - "Запрос ушёл, status 200"
          - "На карте видны линии, но из GeoJSON-source (gps-tracks-layer-geo)"

      - id: E2E-DEPLOY-05
        name: "Регрессия: popup и скачивание GPX"
        steps:
          - "window._map.setZoom(8)"
          - "Кликнуть по треку из источника osm"
          - "Popup открылся, в нём есть кнопка 'Скачать GPX'"
          - "Клик по кнопке скачивает .gpx файл (ET-011 контракт)"

test_data:
  fixtures_dir: "tests/fixtures/gps-tracks/"
  fixtures:
    - name: "mvt-z8-snapshot.json"
      description: "Snapshot число features в тайле z=8/154/79 над Москвой до ET-012 (для IT-REGRESS-Z8-01)"
    - name: "mvt-z10-snapshot.json"
      description: "Аналогично для z=10/617/319 (IT-REGRESS-Z10-01)"
  notes:
    - "Snapshot'ы создаются разово до начала разработки ET-012 на текущем состоянии test-БД и кладутся в репо."
    - "Для unit-тестов использовать sqlite3.Row mock — реальная БД не нужна."

test_environment:
  unit:
    - "pytest tmp_path для временной sqlite (по необходимости)"
    - "Mock sqlite3.Row через unittest.mock или фабрика"
  integration:
    - "Test sqlite БД с фикстурными треками из existing ET-008/009 фабрик"
    - "FastAPI TestClient для endpoint вызовов"
  performance:
    - "Маркер @pytest.mark.perf, не в обычном CI"
    - "Запуск перед merge: pytest -m perf"
  e2e:
    - "Test-среда https://openclaw.mva154.duckdns.org/enduro/"
    - "Реальная БД после ET-009 прогона"
    - "UI-тесты — см. 04b-ui-test-cases.md (Playwright)"

ci_gates:
  - "Unit-тесты UT-Z*-* и UT-SIMP-* — обязательны (AC-11, AC-12)"
  - "Integration IT-Z*-*, IT-CACHE-*, IT-REGRESS-* — обязательны (AC-13)"
  - "Регрессия RG-* — обязательна (AC-14)"
  - "Performance PERF-Z5-01 — обязателен перед merge (AC-19)"
  - "UI-тесты — запуск после деплоя, фиксация в 13-test-report.md"
  - "E2E-DEPLOY-* — ручные шаги в 14-deploy-log.md"
---
