433 lines
20 KiB
YAML
433 lines
20 KiB
YAML
---
|
||
type: test-plan
|
||
work_item_id: ET-009
|
||
title: "Test Plan: Новые источники GPS-треков — EnduroRussia и Wikiloc"
|
||
version: 1
|
||
status: draft
|
||
created_at: 2026-06-01
|
||
updated_at: 2026-06-01
|
||
authors:
|
||
- "agent:analyst"
|
||
related:
|
||
- "ET-008"
|
||
|
||
scope_note: >
|
||
ET-009 не строит новую инфраструктуру; цель — активировать два
|
||
новых источника (EnduroRussia, Wikiloc) в существующем pipeline
|
||
ET-008. Тест-план фокусируется на (1) корректности парсеров на
|
||
реальных фикстурах, (2) лицензионном guard'е, (3) дедупликации
|
||
межисточниковых пересечений, (4) первом продакшн-прогоне с
|
||
отчётностью, (5) непротиворечивости UI. Регрессия ET-008 проверяется
|
||
существующим test_plan ET-008.
|
||
|
||
test_suites:
|
||
|
||
- name: unit-enduro-russia-parser
|
||
type: unit
|
||
description: "EnduroRussiaParser на фикстурах"
|
||
cases:
|
||
- id: UT-ER-01
|
||
name: "_parse_gpx из enduro-russia-track-1.gpx — успех"
|
||
input: "GPX-фикстура с ≥ 10 trkpt, координаты внутри ЦФО"
|
||
expected: |
|
||
TrackInsert.points_count ≥ 10,
|
||
length_m > 0,
|
||
min_lon/max_lon корректны,
|
||
external_url = 'https://endurorussia.ru/tracks/<id>',
|
||
source_id = 'enduro_russia'
|
||
|
||
- id: UT-ER-02
|
||
name: "_parse_gpx из enduro-russia-track-2.gpx (пустой) → None"
|
||
input: "GPX-фикстура с 0 trkpt"
|
||
expected: "_parse_gpx возвращает None"
|
||
|
||
- id: UT-ER-03
|
||
name: "Bbox-фильтр отсеивает enduro-russia-track-3.gpx"
|
||
input: "GPX с точкой за пределами bbox ЦФО"
|
||
expected: "_bbox_intersects → False; collect() не yield-ит этот трек"
|
||
|
||
- id: UT-ER-04
|
||
name: "MAPPING категорий"
|
||
input: "difficulty ∈ {'hard', 'soft', 'мото', 'unknown'}"
|
||
expected: |
|
||
'hard' → 'enduro'
|
||
'soft' → 'enduro'
|
||
'мото' → 'moto'
|
||
'unknown' → 'other' (через map_activity default)
|
||
|
||
- id: UT-ER-05
|
||
name: "Конфиг base_url без дефиса (регрессия R-4)"
|
||
input: "source_config = {'base_url': 'https://endurorussia.ru', ...}"
|
||
expected: |
|
||
parser.config['base_url'] == 'https://endurorussia.ru'
|
||
(без дефиса). HTTP-запросы в collect() уходят на endurorussia.ru.
|
||
|
||
- id: UT-ER-06
|
||
name: "Pagination завершается при fetched_so_far >= total"
|
||
input: "Mock API: total=5, page 0 возвращает 5 items, page 1 не должен запрашиваться"
|
||
expected: "collect() сделал 1 запрос /api/tracks, не 2+"
|
||
|
||
- id: UT-ER-07
|
||
name: "HTTP 429 на /api/tracks — graceful return"
|
||
input: "Mock 429 на первой странице"
|
||
expected: "collect() завершается, exception не пробрасывается, 0 yield-ов"
|
||
|
||
- id: UT-ER-08
|
||
name: "HTTP 429 на /api/tracks/{id}/gpx — graceful return, ранние треки сохранены"
|
||
input: "Mock: первая страница ОК (3 GPX), на 4-м GPX → 429"
|
||
expected: "collect() yield-ит 3 трека, затем завершается без exception"
|
||
|
||
- name: unit-wikiloc-parser
|
||
type: unit
|
||
description: "WikilocParser на фикстурах"
|
||
cases:
|
||
- id: UT-WL-01
|
||
name: "_extract_track_paths из wikiloc-search-page1.html"
|
||
input: "HTML-фикстура с ≥ 5 ссылками на треки"
|
||
expected: "Возвращён список из ≥ 5 уникальных строк вида '/trails/<slug>/<id>'"
|
||
|
||
- id: UT-WL-02
|
||
name: "_extract_gpx_url: downloadTrail.do"
|
||
input: "HTML с 'downloadTrail.do?id=12345'"
|
||
expected: "Возвращён 'https://www.wikiloc.com/wikiloc/downloadTrail.do?id=12345'"
|
||
|
||
- id: UT-WL-03
|
||
name: "_extract_gpx_url: fallback по track_id"
|
||
input: "HTML без явных ссылок на GPX, track_id='99999'"
|
||
expected: "Возвращён 'https://www.wikiloc.com/wikiloc/downloadTrail.do?id=99999'"
|
||
|
||
- id: UT-WL-04
|
||
name: "_extract_track_name: <h1>"
|
||
input: "HTML с '<h1>Test Trail</h1>'"
|
||
expected: "Возвращена строка 'Test Trail'"
|
||
|
||
- id: UT-WL-05
|
||
name: "_parse_gpx из wikiloc-track.gpx — успех"
|
||
input: "GPX-фикстура Wikiloc"
|
||
expected: |
|
||
TrackInsert.activity_type == 'moto' (для активности 'motorcycle'),
|
||
source_id == 'wikiloc',
|
||
external_url содержит 'wikiloc.com'
|
||
|
||
- id: UT-WL-06
|
||
name: "MAPPING категорий"
|
||
input: "{'motorcycle', 'hiking', 'mtb'}"
|
||
expected: |
|
||
motorcycle → moto
|
||
hiking → hike
|
||
mtb → bicycle
|
||
|
||
- id: UT-WL-07
|
||
name: "HTTP 403 на странице поиска — graceful stop"
|
||
input: "Mock: первая страница поиска → 403"
|
||
expected: "collect() возвращается без exception, 0 yield-ов"
|
||
|
||
- id: UT-WL-08
|
||
name: "HTTP 429 на странице трека — graceful stop, ранние сохранены"
|
||
input: "Mock: поиск ОК, 1-й трек ОК, на 2-м → 429"
|
||
expected: "collect() yield-ит 1 трек, затем завершается без exception"
|
||
|
||
- id: UT-WL-09
|
||
name: "rate_limit соблюдается"
|
||
input: "asyncio.sleep mock; парсер с rate_limit_sec=10"
|
||
expected: |
|
||
asyncio.sleep вызван между запросами с аргументом ≥ 10.
|
||
Минимум 2 вызова asyncio.sleep на 2 трека.
|
||
|
||
- id: UT-WL-10
|
||
name: "max_tracks_per_run кап"
|
||
input: "Mock поиск выдаёт 5 треков, max_tracks_per_run=2"
|
||
expected: "collect() yield-ит ровно 2 трека и завершается"
|
||
|
||
- name: unit-config-loader
|
||
type: unit
|
||
description: "Расширения существующего config-loader"
|
||
cases:
|
||
- id: UT-CFG-01
|
||
name: "gps_sources.yaml парсится с записью wikiloc"
|
||
input: "Текущий config/gps_sources.yaml после правок ET-009"
|
||
expected: |
|
||
load_sources_config возвращает список с id ∈ {osm, enduro_russia, wikiloc, ttrails}.
|
||
wikiloc.enabled == True.
|
||
enduro_russia.base_url == 'https://endurorussia.ru'.
|
||
|
||
- id: UT-CFG-02
|
||
name: "gps_regions.yaml содержит wikiloc"
|
||
input: "Текущий config/gps_regions.yaml после правок ET-009"
|
||
expected: |
|
||
tsfo_plus_chuvashia.sources contains 'wikiloc' and 'enduro_russia'.
|
||
|
||
- id: UT-CFG-03
|
||
name: "Невалидный rate_limit_sec ≤ 0 → ошибка"
|
||
input: "wikiloc.rate_limit_sec = 0"
|
||
expected: "ConfigError или валидация при load"
|
||
|
||
- name: integration-pipeline-et009
|
||
type: integration
|
||
description: "Pipeline gps_collect.py с mock EnduroRussia + Wikiloc"
|
||
cases:
|
||
- id: IT-ER-01
|
||
name: "Прогон EnduroRussia с 3 фикстурными GPX"
|
||
input: |
|
||
Mock https://endurorussia.ru/api/tracks → enduro-russia-api-tracks-page1.json
|
||
Mock /api/tracks/1/gpx → enduro-russia-track-1.gpx (inside bbox)
|
||
Mock /api/tracks/2/gpx → enduro-russia-track-2.gpx (empty)
|
||
Mock /api/tracks/3/gpx → enduro-russia-track-3.gpx (outside bbox)
|
||
expected: |
|
||
tracks_new == 1 (track-1 прошёл, track-2 None, track-3 filtered)
|
||
pipeline_runs[-1].status == 'ok'
|
||
exit_code == 0
|
||
|
||
- id: IT-WL-01
|
||
name: "Прогон Wikiloc с 1 фикстурным треком"
|
||
input: |
|
||
Mock /wikiloc/find.do?... → wikiloc-search-page1.html
|
||
Mock /trails/.../12345 → wikiloc-trail-page.html
|
||
Mock /wikiloc/downloadTrail.do?id=12345 → wikiloc-track.gpx
|
||
(остальные ссылки из поиска → 404, чтобы остановиться)
|
||
expected: |
|
||
tracks_new == 1
|
||
pipeline_runs[-1].status ∈ {'ok', 'partial'}
|
||
exit_code == 0
|
||
|
||
- id: IT-WL-02
|
||
name: "Wikiloc graceful-stop на 403"
|
||
input: "Mock /wikiloc/find.do → 403"
|
||
expected: |
|
||
tracks_new == 0
|
||
pipeline_runs[-1].status == 'partial' (не 'error')
|
||
exit_code == 0 (graceful-stop ≠ error)
|
||
|
||
- id: IT-WL-03
|
||
name: "Wikiloc graceful-stop на 429 после первого трека"
|
||
input: "Mock: поиск ОК (2 трека), trail-page для 1-го ОК, GPX 1-го ОК, для 2-го → 429"
|
||
expected: |
|
||
tracks_new == 1
|
||
pipeline_runs[-1].status == 'partial'
|
||
exit_code == 0
|
||
|
||
- id: IT-DEDUP-01
|
||
name: "Dedup-merge: EnduroRussia + Wikiloc один и тот же трек"
|
||
input: |
|
||
1) Pipeline собирает EnduroRussia: 1 трек с bbox X, length L, date D.
|
||
2) Pipeline собирает Wikiloc: 1 трек с bbox X±0.005, length L±2%, date D.
|
||
expected: |
|
||
В БД 1 запись (не 2).
|
||
sources_json содержит ['enduro_russia', 'wikiloc'] (порядок не важен).
|
||
external_urls_json содержит обе ссылки.
|
||
Метаданные (name, activity_type) приоритетно из enduro_russia (priority 80 > 70).
|
||
|
||
- id: IT-DEDUP-02
|
||
name: "Разные даты → разные записи"
|
||
input: "Те же геометрия и длина, но даты отличаются на 5 дней"
|
||
expected: "В БД 2 записи"
|
||
|
||
- id: IT-LIC-01
|
||
name: "Licensing-guard блокирует source при status=proposed"
|
||
input: |
|
||
Подменить ADR-010 на временный файл со status: proposed.
|
||
Запустить pipeline для enduro_russia.
|
||
expected: |
|
||
tracks_new == 0
|
||
pipeline_runs[-1].status == 'skipped_license'
|
||
exit_code == 1 (has_error)
|
||
|
||
- id: IT-LIC-02
|
||
name: "Licensing-guard пропускает source при status=accepted"
|
||
input: "Обычный ADR-010 со status: accepted"
|
||
expected: |
|
||
pipeline загружает parser и пытается собирать.
|
||
status НЕ 'skipped_license'.
|
||
|
||
- name: contract-endurorussia-api
|
||
type: contract
|
||
description: "Реальные запросы к endurorussia.ru — nightly-only"
|
||
marker: "@pytest.mark.network"
|
||
cases:
|
||
- id: CT-ER-01
|
||
name: "GET /api/tracks?page=0&limit=5 → 200 + JSON"
|
||
input: "Реальный HTTPS-запрос с UA enduro-trails"
|
||
expected: |
|
||
status_code == 200
|
||
response.json() имеет ключи: items (list), total (int)
|
||
len(items) > 0
|
||
items[0] имеет ключи: id (int), name (str)
|
||
|
||
- id: CT-ER-02
|
||
name: "GET /api/tracks/{first_id}/gpx → 200 + parseable GPX"
|
||
input: "first_id из CT-ER-01"
|
||
expected: |
|
||
status_code == 200
|
||
Content-Type содержит 'xml' или 'gpx'
|
||
defusedxml.fromstring(response.content) не бросает exception
|
||
Root tag заканчивается на 'gpx'
|
||
|
||
- name: contract-wikiloc
|
||
type: contract
|
||
description: "Реальный smoke-тест Wikiloc — ручной, не в CI"
|
||
marker: "manual"
|
||
cases:
|
||
- id: CT-WL-01
|
||
name: "Wikiloc find.do возвращает HTML с трек-ссылками"
|
||
input: |
|
||
Один curl-запрос с UA enduro-trails:
|
||
GET https://www.wikiloc.com/wikiloc/find.do?act=19&sw=55,37&ne=56,38&page=0
|
||
expected: |
|
||
status_code == 200
|
||
HTML содержит ≥ 1 совпадение '/trails/'
|
||
Результат фиксируется в 13-test-report.md, скриншот сохраняется в docs/work-items/ET-009/.
|
||
|
||
- name: integration-api-endpoint
|
||
type: integration
|
||
description: "Endpoint /api/gps-tracks после ET-009 — новые ID источников"
|
||
cases:
|
||
- id: IT-API-01
|
||
name: "Ответ содержит features с source 'enduro_russia'"
|
||
input: |
|
||
Подготовка: вставить в test-БД 5 треков с source_id='enduro_russia'.
|
||
GET /api/gps-tracks?bbox=37,55,38,56
|
||
expected: |
|
||
status 200
|
||
features[].properties.sources содержит 'enduro_russia' хотя бы для одного
|
||
|
||
- id: IT-API-02
|
||
name: "Ответ содержит features с source 'wikiloc'"
|
||
input: "Аналогично с wikiloc"
|
||
expected: "features[].properties.sources содержит 'wikiloc'"
|
||
|
||
- id: IT-API-03
|
||
name: "Фильтр ?source=enduro_russia"
|
||
input: "Тест-БД 5 enduro_russia + 5 wikiloc + 5 osm"
|
||
expected: |
|
||
status 200
|
||
количество features ровно 5
|
||
все sources == ['enduro_russia']
|
||
|
||
- id: IT-API-04
|
||
name: "Health: tracks_by_source включает оба новых ID"
|
||
input: "GET /api/gps-tracks/health после подготовки"
|
||
expected: |
|
||
status 200
|
||
tracks_by_source.enduro_russia ≥ 1
|
||
tracks_by_source.wikiloc ≥ 1
|
||
|
||
- name: e2e-first-production-run
|
||
type: e2e
|
||
description: "Первый ручной прогон в test-среде"
|
||
marker: "manual"
|
||
cases:
|
||
- id: E2E-PROD-01
|
||
name: "EnduroRussia: первый прогон собирает ≥ 200 треков"
|
||
steps:
|
||
- "ssh mva154"
|
||
- "cd /opt/enduro-trails"
|
||
- "Проверить наличие data/gps_tracks.sqlite (или ожидать создания)"
|
||
- "Запустить: python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia"
|
||
- "Дождаться завершения (≤ 45 мин)"
|
||
- "Проверить exit code = 0"
|
||
- "Запрос: sqlite3 data/gps_tracks.sqlite 'SELECT COUNT(*) FROM tracks WHERE sources_json LIKE \"%enduro_russia%\"'"
|
||
- "Ожидаемо: count ≥ 200"
|
||
- "Зафиксировать длительность и tracks_new в 14-deploy-log.md"
|
||
|
||
- id: E2E-PROD-02
|
||
name: "Wikiloc: первый прогон собирает ≥ 1 трек"
|
||
steps:
|
||
- "Запустить: python scripts/gps_collect.py --region tsfo_plus_chuvashia --source wikiloc"
|
||
- "Дождаться (≤ 30 мин при max_tracks_per_run=50)"
|
||
- "Проверить exit code = 0"
|
||
- "sqlite3 ... 'SELECT COUNT(*) FROM tracks WHERE sources_json LIKE \"%wikiloc%\"'"
|
||
- "Ожидаемо: count ≥ 1"
|
||
- "Зафиксировать в 14-deploy-log.md (включая если 0 — отдельно отметить как fail E2E-PROD-02)"
|
||
|
||
- id: E2E-PROD-03
|
||
name: "Health-эндпоинт показывает новые источники"
|
||
steps:
|
||
- "curl https://openclaw.mva154.duckdns.org/enduro/api/gps-tracks/health"
|
||
- "Проверить наличие ключей tracks_by_source.enduro_russia и tracks_by_source.wikiloc"
|
||
|
||
- id: E2E-PROD-04
|
||
name: "Нет 'enduro-russia.ru' (с дефисом) в external_urls"
|
||
steps:
|
||
- "sqlite3 data/gps_tracks.sqlite \"SELECT COUNT(*) FROM tracks WHERE external_urls_json LIKE '%enduro-russia.ru%'\""
|
||
- "Ожидаемо: 0 (или результаты пометить для опционального UPDATE-скрипта)"
|
||
|
||
- name: regression-et008
|
||
type: regression
|
||
description: "Регрессия ET-008 — все существующие тесты остаются зелёными"
|
||
cases:
|
||
- id: RG-08-01
|
||
name: "Все unit-тесты ET-008 проходят"
|
||
input: "pytest tests/unit/ -v"
|
||
expected: "Все тесты gps-tracks из ET-008 (U-01..U-62) проходят"
|
||
|
||
- id: RG-08-02
|
||
name: "Все integration-тесты ET-008 проходят"
|
||
input: "pytest tests/integration/ -v"
|
||
expected: "I-01..I-57 проходят"
|
||
|
||
- id: RG-08-03
|
||
name: "Все e2e-тесты ET-008 проходят"
|
||
input: "pytest tests/e2e/ -v (или соответствующий маркер)"
|
||
expected: "E-01..E-41 проходят"
|
||
|
||
- name: load-baseline
|
||
type: load
|
||
description: "Производительность endpoint не деградировала"
|
||
cases:
|
||
- id: L-01
|
||
name: "p95 /api/gps-tracks ≤ 300 мс"
|
||
input: "100 параллельных клиентов, по 100 запросов, z=10, bbox с ~500 треков"
|
||
expected: "p95 latency ≤ 300 ms"
|
||
|
||
- id: L-02
|
||
name: "p95 /api/gps-tracks/tiles ≤ 300 мс (cold)"
|
||
input: "100 уникальных тайлов z=8..11"
|
||
expected: "p95 cold ≤ 300 ms; hit-rate кэша > 80% на повторах"
|
||
|
||
test_data:
|
||
fixtures_dir: "tests/fixtures/gps-tracks/"
|
||
fixtures:
|
||
- name: "enduro-russia-api-tracks-page1.json"
|
||
description: "Реальный snapshot ответа GET /api/tracks?page=0&limit=50, ≥ 5 items"
|
||
source: "manual curl до начала разработки"
|
||
- name: "enduro-russia-track-1.gpx"
|
||
description: "GPX с ≥ 10 trkpt, координаты в ЦФО"
|
||
- name: "enduro-russia-track-2.gpx"
|
||
description: "GPX пустой (для skip-логики)"
|
||
- name: "enduro-russia-track-3.gpx"
|
||
description: "GPX за пределами bbox ЦФО (для bbox-фильтра)"
|
||
- name: "wikiloc-search-page1.html"
|
||
description: "Snapshot страницы поиска Wikiloc, ≥ 5 ссылок"
|
||
- name: "wikiloc-trail-page.html"
|
||
description: "Snapshot страницы одного трека Wikiloc"
|
||
- name: "wikiloc-track.gpx"
|
||
description: "GPX из Wikiloc (для dedup-merge с EnduroRussia)"
|
||
|
||
test_environment:
|
||
unit:
|
||
- "Mock HTTP через respx или httpx_mock"
|
||
- "asyncio.sleep моссится для UT-WL-09"
|
||
- "Temporary sqlite через pytest tmp_path"
|
||
integration:
|
||
- "Mock HTTP-сервер для EnduroRussia и Wikiloc URLs"
|
||
- "Изолированная sqlite в tmp_path"
|
||
contract:
|
||
- "Маркер @pytest.mark.network — пропускается в CI по умолчанию"
|
||
- "Запуск nightly или вручную: pytest -m network"
|
||
e2e:
|
||
- "Test-среда https://openclaw.mva154.duckdns.org/enduro/"
|
||
- "Доступ ssh mva154 у оператора Деплоя"
|
||
- "UI-тесты — см. 04b-ui-test-cases.md (Playwright)"
|
||
load:
|
||
- "k6 или locust против test-среды"
|
||
- "Запускается отдельно, не в обычном CI"
|
||
|
||
ci_gates:
|
||
- "Все unit-тесты ET-009 (UT-ER-*, UT-WL-*, UT-CFG-*) — обязательны"
|
||
- "Все integration-тесты ET-009 (IT-*) — обязательны"
|
||
- "Регрессия ET-008 (RG-08-*) — обязательна"
|
||
- "Contract-тесты (CT-*) — опциональны (network marker)"
|
||
- "E2E ручные (E2E-PROD-*) — выполняются после деплоя, фиксируются в 14-deploy-log.md"
|
||
- "Load-тесты (L-*) — выполняются один раз перед merge"
|
||
---
|