Files
enduro-trails/docs/work-items/ET-009/04-test-plan.yaml
claude-bot eaa6b4cd27
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped
feat(ET-009): analyst artifacts — BRD, TRZ, AC, test plan
2026-06-01 17:51:47 +00:00

433 lines
20 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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"
---