diff --git a/docs/work-items/ET-009/01-brd.md b/docs/work-items/ET-009/01-brd.md new file mode 100644 index 0000000..59b150a --- /dev/null +++ b/docs/work-items/ET-009/01-brd.md @@ -0,0 +1,239 @@ +--- +type: brd +work_item_id: ET-009 +title: "BRD: Новые источники GPS-треков — EnduroRussia и Wikiloc" +version: 1 +status: draft +created_at: 2026-06-01 +updated_at: 2026-06-01 +authors: + - "agent:analyst" +related: + - "ET-008" +--- + +# BRD — ET-009: Новые источники GPS-треков — EnduroRussia и Wikiloc + +## 1. Цель + +Расширить пул реальных GPS-треков, видимых пользователю Enduro Trails, +за счёт **двух новых источников** — `endurorussia.ru` и `wikiloc.com`. +Pipeline сбора, БД, API и UI-слой уже построены в **ET-008**; ET-009 +**не строит инфраструктуру**, а: + +1. **Активирует EnduroRussia.ru** как источник в продакшне (parser-код и + ADR-010 уже готовы, но source находится в `gps_sources.yaml` как + `enabled: false`; конфиг ссылается на `enduro-russia.ru` с + дефисом — расхождение с реальным доменом `endurorussia.ru` без + дефиса требует корректировки). +2. **Включает Wikiloc** как новый источник: добавляет запись в + `gps_sources.yaml`, привязывает к регионам, проверяет + parser/lifecycle/ratelimit и активирует. +3. Гарантирует, что после первого продакшн-прогона в БД + `data/gps_tracks.sqlite` появляются треки с обоих новых источников + и они корректно отдаются пользователю через существующие endpoints + и UI-фильтры. + +ET-009 — **«заявить, подключить, доказать что работает»**, а не новая +функциональность. + +## 2. Контекст + +- **ET-008** разработал и развернул в test: + - `src/api/gps_tracks/` (модели, БД, дедуп, MVT, endpoint, parsers). + - Pipeline `scripts/gps_collect.py` с поддержкой нескольких источников. + - Конфиги `config/gps_sources.yaml` и `config/gps_regions.yaml`. + - UI: чекбокс «Публичные треки», sheet фильтров, popup трека, + halo-слой на спутнике. + - ADR-009/010/011/012 (licensing OSM / EnduroRussia / ttrails / Wikiloc). +- На момент старта ET-009: + - `osm` — `enabled: true`, работает в проде. + - `ttrails` — `enabled: false`, в задаче ET-009 не активируется. + - `enduro_russia` — parser-код есть, ADR-010 `accepted`, но + `gps_sources.yaml` содержит `enabled: false` и URL `enduro-russia.ru` + (с дефисом). Реальный домен по бизнес-требованию — + `endurorussia.ru` (без дефиса), это подтверждает и parser-код + (`src/api/gps_tracks/sources/enduro_russia.py` default + `https://endurorussia.ru`). + - `wikiloc` — parser-код есть, ADR-012 `accepted`, но в + `gps_sources.yaml` **отсутствует**. +- API EnduroRussia: открытый JSON, без авторизации, 305+ треков по РФ: + - `GET https://endurorussia.ru/api/tracks?page=N&limit=50` + - `GET https://endurorussia.ru/api/tracks/{id}/gpx` +- Wikiloc: публичного API нет, доступ только через HTML-парсинг + страниц поиска и треков; rate-limit жёсткий — 10 сек между + запросами; при 403/429 — graceful-stop. + +## 3. Scope + +### In scope + +| # | Функция | +| ----- | ------------------------------------------------------------------------------------------------------ | +| F-01 | Исправление `gps_sources.yaml`: `enduro_russia.base_url` → `https://endurorussia.ru` (без дефиса). | +| F-02 | `gps_sources.yaml`: `enduro_russia.enabled` → `true`. | +| F-03 | Верификация ADR-010 (`accepted`) на момент активации — pipeline-guard должен пропустить source. | +| F-04 | Добавление в `gps_sources.yaml` записи `wikiloc` с `enabled: true`, `rate_limit_sec: 10`, `license_adr: docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md`. | +| F-05 | Обновление `config/gps_regions.yaml`: `tsfo_plus_chuvashia.sources` дополняется значением `wikiloc` (osm уже есть, enduro_russia уже есть). | +| F-06 | Интеграционные тесты на parser `enduro_russia.py` с фикстурами реальных ответов API: 1 страница списка + 3 GPX-файла + edge cases. | +| F-07 | Интеграционные тесты на parser `wikiloc.py` с фикстурами реальных HTML-страниц: страница поиска, страница трека, GPX. | +| F-08 | Тесты dedup-merge на пару (osm-трек, enduro_russia-трек) с одной поездкой → одна запись с `sources=['osm','enduro_russia']`. | +| F-09 | Тесты graceful-stop wikiloc на 403/429: парсер останавливается, не падает, `pipeline_runs.status='partial'` или `'rate_limited'`. | +| F-10 | Health-эндпоинт `/api/gps-tracks/health` после прогона показывает `tracks_by_source` с ненулевыми значениями для `enduro_russia` и `wikiloc`. | +| F-11 | UI: фильтр «Источник» в `#sheet-gps-filters` динамически отображает 3 чекбокса — OSM, EnduroRussia, Wikiloc — по данным API. | +| F-12 | Атрибуция: в правом нижнем углу карты MapLibre Attribution содержит «EnduroRussia.ru» и «© Wikiloc contributors» при наличии треков из этих источников. | +| F-13 | Цветовая палитра по источнику в `style.json`/`style-dark.json` содержит цвета для `enduro_russia` и `wikiloc` (а не только OSM). | +| F-14 | Первый продакшн-прогон pipeline на test-сервере для региона `tsfo_plus_chuvashia`: собирает ≥ 200 треков с EnduroRussia и пробует Wikiloc (любое ненулевое количество приемлемо ввиду rate-limit). | + +### Out of scope + +- **Активация ttrails** (Тропинки.ру) — отдельный work item. +- **Изменение схемы БД** — структура `gps_tracks.sqlite` остаётся как в ET-008. +- **Новые поля метаданных** — что собираем по каждому треку, определено ET-008. +- **Wikiloc Premium / OAuth** — пользуемся только публичными HTML. +- **Расширение алгоритма дедупликации** — берём как есть из ET-008. +- **Запуск автоматического cron** — расписание cron включается отдельным task'ом + после успешного ручного прогона (см. F-14). ET-009 ограничивается ручным + `python scripts/gps_collect.py --region tsfo_plus_chuvashia`. +- **Удаление stale-треков** (GC) — отдельный концерн pipeline, не активируется в ET-009. +- **Расширение на новые регионы** — Северный Кавказ остаётся `enabled: false`. + +## 4. Источники — детальное описание + +### 4.1 EnduroRussia.ru + +| Параметр | Значение | +| -------------------------- | ----------------------------------------------------------------------------------- | +| Тип доступа | Публичный JSON API без авторизации | +| Базовый URL | `https://endurorussia.ru` | +| Endpoint list | `GET /api/tracks?page=&limit=50` → `{items: [{id, name, difficulty, …}], total}` | +| Endpoint GPX | `GET /api/tracks/{id}/gpx` → GPX 1.1 XML | +| Объём | ≥ 305 публичных треков (на момент составления BRD) | +| География | Россия, преимущественно ЦФО, эндуро-категория | +| Активность | enduro, мото, hard, soft, тур → MAPPING → `enduro`/`moto` | +| ToS | Публичные треки; нет явного запрета на программный доступ; см. ADR-010 | +| robots.txt | Не запрещает `/api/` для программного доступа с явным UA (см. ADR-010 §2) | +| Attribution | «EnduroRussia.ru» в строке атрибуции карты | +| Rate-limit | 5 сек между запросами (`rate_limit_sec: 5`) | +| save_user_field | `false` — автор не сохраняется (ADR-010 §3) | + +### 4.2 Wikiloc + +| Параметр | Значение | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Тип доступа | Парсинг публичных HTML-страниц (API недоступно) | +| Базовый URL | `https://www.wikiloc.com` | +| Endpoint поиска | `GET /wikiloc/find.do?act=&sw=&ne=&page=` → HTML с `` | +| Endpoint трека | `GET /trails//` → HTML c ссылкой на GPX | +| Endpoint GPX | `GET /wikiloc/downloadTrail.do?id=` → GPX XML | +| Активности (act код) | motorcycle=19, enduro=19, mtb=3 | +| ToS | Треки публичные; ADR-012 фиксирует условия некоммерческого использования | +| robots.txt | Не запрещает страницы треков с явным UA (см. ADR-012 §2) | +| Attribution | «© Wikiloc contributors» в строке атрибуции карты | +| Rate-limit | **10 сек** между запросами (`rate_limit_sec: 10`) — жёстко | +| Graceful-stop | При HTTP 403/429 — немедленный stop без ретраев, статус прогона `rate_limited` или `partial` | +| Хрупкость | HTML-парсер. При смене структуры — парсер вернёт 0 треков без краша. См. риск R-1. | +| save_user_field | `false` — автор не сохраняется (ADR-012 §5) | + +### 4.3 Контроль licensing + +Pipeline-guard `_check_license_adr()` уже реализован (см. +`scripts/gps_collect.py` строки 37–73): при `enabled: true` source +загружается только если `license_adr.status == 'accepted'`. Перед +активацией ET-009 **обязательно перечитать** ADR-010 и ADR-012 и +убедиться, что обе ADR имеют `status: accepted` в YAML front-matter. +Если на момент работы ET-009 одна из ADR оказалась в другом статусе — +работу остановить, эскалировать архитектору. + +## 5. Метрики успеха + +| Метрика | Критерий | +| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| Конфиг корректен | `gps_sources.yaml` содержит запись `enduro_russia` с `base_url: https://endurorussia.ru` (без дефиса) и `enabled: true`. | +| Wikiloc заведён | `gps_sources.yaml` содержит запись `wikiloc` с `enabled: true`, `rate_limit_sec: 10`, `license_adr: …ADR-012…`. | +| Регион подписан | `gps_regions.yaml` для `tsfo_plus_chuvashia` содержит `wikiloc` в `sources`. `enduro_russia` уже подписан. | +| Pipeline-guard работает | При `status: proposed` в ADR-010 (искусственно) — pipeline пропускает source с `pipeline_runs.status='skipped_license'`. | +| Покрытие EnduroRussia | После прогона: `tracks_by_source.enduro_russia ≥ 200` (исходим из ≥ 305 публичных треков с учётом фильтра bbox региона). | +| Покрытие Wikiloc | После прогона: `tracks_by_source.wikiloc ≥ 1` (rate-limit 10 сек × ≥ 3 запроса на трек делает сбор медленным; любое ненулевое значение приемлемо для validation того, что парсер работает end-to-end). | +| Дедупликация работает | Среди ≥ 200 треков EnduroRussia: записи с `sources=['osm','enduro_russia']` или `sources=['enduro_russia','wikiloc']` существуют (хотя бы 1 в выборке). | +| Graceful-stop | Mock-эмуляция HTTP 403 / 429 от Wikiloc в integration-тесте → pipeline не падает, статус прогона `rate_limited` или `partial`. | +| Атрибуция | В правом нижнем углу карты после включения слоя видны строки «EnduroRussia.ru» и «© Wikiloc contributors». | +| UI-фильтр источников | В `#sheet-gps-filters` после первого прогона видны минимум 3 чекбокса: OSM / EnduroRussia / Wikiloc; снятие галки с источника убирает соответствующие линии. | +| Производительность не деградировала | `/api/gps-tracks?bbox=…` p95 не вырос относительно ET-008 baseline (≤ 300 мс на z ≥ 10, ≤ 500 треков в bbox). | +| Чистый health | `/api/gps-tracks/health` возвращает `last_run_status='ok'` или `'partial'` (не `'error'`), `errors_count == 0` или ≤ 5%. | + +## 6. Риски + +| # | Риск | Вероятность | Влияние | Митигация | +| --- | ----------------------------------------------------------------------------------- | ----------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| R-1 | Wikiloc меняет HTML → парсер возвращает 0 треков | Высокая | Среднее | Парсер уже спроектирован graceful: возвращает 0, не падает. Health-эндпоинт показывает 0 в `tracks_by_source.wikiloc` → видимый сигнал. | +| R-2 | Wikiloc банит IP mva154 | Средняя | Высокое | Rate-limit 10 сек + UA с контактом + graceful-stop на 403/429. После активации мониторим первые 3 прогона; при систематических 403 — `enabled: false` и эскалация. | +| R-3 | EnduroRussia API меняет схему ответа | Низкая | Среднее | Parser проверяет наличие ключевых полей (`items`, `id`); при KeyError — `tracks_new=0`, статус `error`. Контрактный тест на JSON. | +| R-4 | Расхождение конфига `enduro-russia.ru` vs реального `endurorussia.ru` | Случилось | Высокое | F-01: исправляем `gps_sources.yaml` сразу. Регрессионный тест: parser отвечает на `https://endurorussia.ru` (не на `enduro-russia.ru`). | +| R-5 | EnduroRussia треки уже содержат `creator=Wikiloc` в GPX → массовые дубли при включении Wikiloc | Высокая | Среднее | ADR-012 §4 явно фиксирует. Тест dedup-merge: одна и та же поездка из enduro_russia и wikiloc → одна запись, `sources` объединён. | +| R-6 | Cron первого прогона превышает окно (≥ 6 часов из-за rate-limit Wikiloc 10 сек × 305 EnduroRussia × 3 запроса/трек) | Средняя | Низкое | EnduroRussia: 305 треков × 5 сек ≈ 25 минут — окей. Wikiloc: per-source максимум `max_tracks_per_run: 50` в первом прогоне (cap в конфиге). | +| R-7 | UI-фильтр «Источник» не подхватывает новые ID | Низкая | Среднее | UI динамически строит фильтр из API (`/api/gps-tracks?stats=true` или из выгрузки) — изменений в коде клиента не требуется. Проверка через UI-тест TC-UI-04 (расширен в ET-009). | +| R-8 | Цветовая палитра в стилях карты не содержит `enduro_russia`/`wikiloc` → линии серым | Высокая | Низкое | F-13: добавить цвета в `style.json`/`style-dark.json` (match-expression `line-color` по `get source`). | +| R-9 | Дамп БД (если есть резервная копия с старым `enduro-russia.ru` URL в `external_url`) — orphan-записи | Низкая | Низкое | До первого прогона новой версии: оператор может выполнить `UPDATE tracks SET external_urls_json = REPLACE(external_urls_json, 'enduro-russia.ru', 'endurorussia.ru')`. Опционально, в `14-deploy-log.md`. | +| R-10| ADR-010 / ADR-012 регрессировали в `proposed` | Низкая | Высокое | F-03: pre-check на момент активации. Если ADR не accepted — задача останавливается, эскалация архитектору. | + +## 7. Зависимости + +### Backend + +- `src/api/gps_tracks/sources/enduro_russia.py` — **код существует** (ET-008). + Изменения возможны только при выявлении бага во время тестов F-06/F-08. +- `src/api/gps_tracks/sources/wikiloc.py` — **код существует** (ET-008). + Изменения возможны только при выявлении бага во время F-07/F-09. +- `scripts/gps_collect.py` — без изменений, используется как есть. +- `src/api/gps_tracks/db.py`, `dedup.py`, `endpoint.py`, `mvt.py` — без + изменений. + +### Конфиги + +- `config/gps_sources.yaml` — изменение F-01..F-04. +- `config/gps_regions.yaml` — изменение F-05. + +### Фронтенд + +- `src/web/style.json` и `src/web/style-dark.json` — F-13: расширить + match-expression `line-color` для слоя `gps-tracks-layer`. +- `src/web/gps_tracks.js` (или модуль ET-008) — **без изменений кода** + при условии, что фильтр-список источников строится из ответа API + динамически. Если в ET-008 список захардкожен — добавить + `enduro_russia` и `wikiloc` в маппинг лейблов источников и палитру. + Это будет уточнено в TRZ §3. + +### Тестовые фикстуры + +- `tests/fixtures/gps-tracks/enduro-russia-api-tracks-page1.json` — реальный snapshot ответа `/api/tracks?page=0`. +- `tests/fixtures/gps-tracks/enduro-russia-track-{1,2,3}.gpx` — три GPX. +- `tests/fixtures/gps-tracks/wikiloc-search-page1.html` — HTML страницы поиска. +- `tests/fixtures/gps-tracks/wikiloc-trail-page.html` — HTML страницы трека. +- `tests/fixtures/gps-tracks/wikiloc-track.gpx` — GPX из Wikiloc. +- `tests/fixtures/gps-tracks/wikiloc-rate-limited.html` — заглушка для 429-сценария. + +### Инфра + +- mva154: исходящие HTTPS к `endurorussia.ru` и `www.wikiloc.com` + (уже разрешены DevOps-политикой). +- Размер `data/gps_tracks.sqlite` не превысит 100 MB после первого + прогона (200 треков × ~50 KB средний размер геометрии). + +### Документация + +- BRD/TRZ/AC/Test-plan этого work item. +- Опциональный ADR `06-adr/ADR-013-domain-fix-enduro-russia.md` — + если расхождение конфиг/реальность сочтено архитектурным решением, + а не баг-фиксом. По умолчанию — это bugfix, ADR не нужен. +- Дополнения к `14-deploy-log.md` после первого прогона: команда + запуска, `tracks_by_source`, длительность. + +### Связи с другими work items + +- **ET-008** — родительская задача; ET-009 расширяет её. Никаких + изменений в артефактах ET-008 не делаем. +- **ttrails** — отдельный work item на активацию третьего источника + (после ET-009). +- **PH-3 Smart Route** — растущая база публичных треков может в будущем + улучшить smart-route. Не в scope. diff --git a/docs/work-items/ET-009/02-trz.md b/docs/work-items/ET-009/02-trz.md new file mode 100644 index 0000000..12a846e --- /dev/null +++ b/docs/work-items/ET-009/02-trz.md @@ -0,0 +1,452 @@ +--- +type: trz +work_item_id: ET-009 +title: "ТЗ: Новые источники GPS-треков — EnduroRussia и Wikiloc" +version: 1 +status: draft +created_at: 2026-06-01 +updated_at: 2026-06-01 +authors: + - "agent:analyst" +related: + - "ET-008" +--- + +# ТЗ — ET-009: Новые источники GPS-треков — EnduroRussia и Wikiloc + +## 1. Терминология + +- **Source** — внешний поставщик GPS-треков, описан записью в + `config/gps_sources.yaml`. Реализуется python-классом-наследником + `SourceParser` в `src/api/gps_tracks/sources/.py`. +- **Region** — географическая область сбора, описана записью в + `config/gps_regions.yaml`. Содержит `bbox` и список активных + `sources` для этой области. +- **Pipeline-guard** — проверка `_check_license_adr()` в + `scripts/gps_collect.py`, которая блокирует загрузку source-парсера + если его ADR в `license_adr` имеет `status != 'accepted'`. +- **Activity-mapping** — словарь `MAPPING` в каждом parser-модуле, + переводящий внутренние категории источника в каноничные + `ACTIVITY_TYPES` (`src/api/gps_tracks/models.py`). +- **Dedup-key** — детерминированный ключ, по которому треки из разных + источников сливаются в одну запись (реализация в + `src/api/gps_tracks/dedup.py:compute_dedup_key`, ET-008). +- **Graceful-stop** — поведение Wikiloc-парсера при HTTP 403/429: + `return` из async-генератора без `raise`, что приводит к статусу + прогона `partial` или `rate_limited` без падения процесса. + +## 2. Архитектурные опоры из ET-008 + +ET-009 не строит новых модулей. Используются: + +- `src/api/gps_tracks/sources/base.py:SourceParser` — базовый класс. +- `src/api/gps_tracks/sources/enduro_russia.py:EnduroRussiaParser` — реализован. +- `src/api/gps_tracks/sources/wikiloc.py:WikilocParser` — реализован. +- `scripts/gps_collect.py` — оркестратор pipeline, поддерживает + per-source rate-limit, licensing-guard, dedup, upsert. +- `src/api/gps_tracks/db.py:upsert_track` — merge по `dedup_key`, + объединение `sources` и `external_urls`. +- `src/api/gps_tracks/endpoint.py` — `/api/gps-tracks`, + `/api/gps-tracks/tiles/{z}/{x}/{y}.mvt`, `/api/gps-tracks/health`. +- `src/web/gps_tracks.js` (или эквивалент в ET-008) — клиентский слой + с динамическим фильтром источников. + +ET-009 = **конфиг + фикстуры + тесты + продакшн-прогон**. + +## 3. Требования + +### REQ-F-01 — Конфиг: `enduro_russia.base_url` + +Файл `config/gps_sources.yaml`, запись с `id: enduro_russia`, поле +`base_url` устанавливается в `https://endurorussia.ru` (без дефиса). + +Текущее значение `https://enduro-russia.ru` (с дефисом) считается +багом и должно быть заменено. + +**Acceptance check.** После правки: +```bash +grep "base_url" config/gps_sources.yaml | grep enduro +``` +выводит `base_url: "https://endurorussia.ru"`. + +### REQ-F-02 — Конфиг: `enduro_russia.enabled` + +В той же записи `enabled: true`. + +**Acceptance check.** В `config/gps_sources.yaml` строка `enabled: true` +находится непосредственно под `id: enduro_russia`. + +### REQ-F-03 — Конфиг: запись `wikiloc` + +В `config/gps_sources.yaml` добавляется новая запись с полями: + +```yaml + - id: wikiloc + name: "Wikiloc" + enabled: true + license_adr: "docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md" + base_url: "https://www.wikiloc.com" + rate_limit_sec: 10 + user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)" + attribution: "© Wikiloc contributors" + parser_module: "src.api.gps_tracks.sources.wikiloc" + save_user_field: false + source_priority: 70 + activity_filter: [motorcycle, enduro] + max_tracks_per_run: 50 +``` + +`max_tracks_per_run` — soft-cap для первого прогона, чтобы не тратить +часы на rate-limit (см. BRD R-6); реализуется в parser'е через +счётчик внутри `collect()`. Если поля в parser ещё нет — добавить +поддержку: + +```python +max_tracks = self.config.get("max_tracks_per_run") +yielded = 0 +# в основном цикле перед yield: +if max_tracks is not None and yielded >= max_tracks: + logger.info("Wikiloc: reached max_tracks_per_run=%d, stopping", max_tracks) + return +yielded += 1 +``` + +### REQ-F-04 — Конфиг: регион `tsfo_plus_chuvashia` + +В `config/gps_regions.yaml`, запись `tsfo_plus_chuvashia.sources` +дополняется до `[osm, enduro_russia, wikiloc, ttrails]`. Порядок +важен: `ttrails` остаётся, но он `enabled: false` в sources.yaml — он +автоматически пропускается guard'ом. + +Поле `enabled: true` региона не меняется. + +### REQ-F-05 — Pipeline licensing-guard + +`scripts/gps_collect.py:_check_license_adr` (строки 37–73) **не +изменяется**. Перед активацией ET-009 выполнить: + +```bash +grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md +grep -E "^status:" docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md +``` + +Оба значения должны быть `accepted`. Иначе — `STOP` и эскалация +архитектору. + +### REQ-F-06 — Тест-фикстура EnduroRussia API + +Создаётся файл `tests/fixtures/gps-tracks/enduro-russia-api-tracks-page1.json` +с реальным snapshot ответа `https://endurorussia.ru/api/tracks?page=0&limit=50`. + +Минимальные требования к snapshot: +- ≥ 5 items. +- Каждый item содержит `id` (int), `name` (str), `difficulty` (str), + `created_at` (str ISO). +- Поле `total` (int) присутствует. + +Снимок делается **разово**, вручную через curl, сохраняется в репо; +не зависит от состояния сайта. + +### REQ-F-07 — Тест-фикстуры EnduroRussia GPX + +Создаются 3 файла `tests/fixtures/gps-tracks/enduro-russia-track-{1,2,3}.gpx` +с реальными GPX-файлами из API. Один из них должен: +- содержать `` с ≥ 10 точками; +- лежать в bbox региона `tsfo_plus_chuvashia` (29..47.5 longitude, + 49.5..60.0 latitude); +- иметь creator или metadata, идентифицирующее источник. + +Второй GPX должен быть пустой (``) или с 0 +trkpt — для проверки skip-логики `_parse_gpx`. + +Третий GPX — c одной точкой за пределами bbox — для проверки +bbox-фильтрации. + +### REQ-F-08 — Тест-фикстура Wikiloc HTML страницы поиска + +Файл `tests/fixtures/gps-tracks/wikiloc-search-page1.html` — реальный +снимок `GET /wikiloc/find.do?act=19&sw=…&ne=…&page=0`. Должен +содержать ≥ 5 ссылок на треки в формате `/trails//`. + +### REQ-F-09 — Тест-фикстуры Wikiloc страницы трека и GPX + +- `tests/fixtures/gps-tracks/wikiloc-trail-page.html` — снимок + страницы одного трека Wikiloc; должен содержать `

` с + названием и либо прямую ссылку на `.gpx`, либо + `downloadTrail.do?id=`. +- `tests/fixtures/gps-tracks/wikiloc-track.gpx` — реальный GPX, + возвращаемый `/wikiloc/downloadTrail.do?id=` для трека, + совпадающего по координатам с одним из EnduroRussia-треков — + для теста dedup-merge. +- `tests/fixtures/gps-tracks/wikiloc-rate-limited.html` — пустой + файл (используется в тесте 429, реальный HTML не важен, + достаточно тестового мока httpx, который вернёт 429). + +### REQ-F-10 — Unit-тесты EnduroRussia parser + +Файл `tests/unit/test_gps_tracks_enduro_russia.py` (новый). + +Покрытие: + +- **UT-ER-01.** `_parse_gpx` принимает фикстурный GPX `enduro-russia-track-1.gpx` + → возвращает `TrackInsert` с `points_count >= 10`, + `min_lon/max_lon/min_lat/max_lat` корректны, `length_m > 0`, + `external_url = "https://endurorussia.ru/tracks/"`. +- **UT-ER-02.** `_parse_gpx` принимает фикстуру `enduro-russia-track-2.gpx` + (пустой) → возвращает `None`. +- **UT-ER-03.** Bbox-фильтр: трек 3 (точка за пределами региона) при + пересечении с region bbox → `_bbox_intersects` возвращает + `False`, `collect()` не yield-ит этот трек. +- **UT-ER-04.** `MAPPING` маппит `"hard" → "enduro"`, `"мото" → "moto"`, + `"unknown" → "other"` (default через `map_activity`). +- **UT-ER-05.** `EnduroRussiaParser.__init__` принимает конфиг с + `base_url: "https://endurorussia.ru"` и сохраняет его (без замены + на дефис-вариант). Регрессия для R-4. +- **UT-ER-06.** `collect()` корректно прерывается, когда + `fetched_so_far >= total`. +- **UT-ER-07.** При HTTP 429 на `/api/tracks` — генератор завершается + без exception. +- **UT-ER-08.** При HTTP 429 на `/api/tracks/{id}/gpx` — генератор + завершается без exception, треки, уже yield-нутые до этого, + сохраняются. + +### REQ-F-11 — Unit-тесты Wikiloc parser + +Файл `tests/unit/test_gps_tracks_wikiloc.py` (новый). + +- **UT-WL-01.** `_extract_track_paths` из фикстуры + `wikiloc-search-page1.html` возвращает ≥ 5 уникальных путей. +- **UT-WL-02.** `_extract_gpx_url`: из HTML с `downloadTrail.do?id=X` + возвращает абсолютный URL `https://www.wikiloc.com/wikiloc/downloadTrail.do?id=X`. +- **UT-WL-03.** `_extract_gpx_url`: из HTML без явных ссылок + возвращает fallback `https://www.wikiloc.com/wikiloc/downloadTrail.do?id=`. +- **UT-WL-04.** `_extract_track_name` извлекает текст `

`. +- **UT-WL-05.** `_parse_gpx` на фикстуре `wikiloc-track.gpx` возвращает + `TrackInsert` с правильными bbox и `activity_type='moto'` (для + activity-категории `motorcycle`). +- **UT-WL-06.** `MAPPING` маппит `"motorcycle" → "moto"`, + `"hiking" → "hike"`, `"mtb" → "bicycle"`. +- **UT-WL-07.** `collect()` останавливается при 403 на странице поиска + (graceful-stop). +- **UT-WL-08.** `collect()` останавливается при 429 на странице трека, + но уже yield-нутые треки сохраняются. +- **UT-WL-09.** Соблюдение `rate_limit_sec`: между двумя + последовательными HTTP-запросами `asyncio.sleep` вызывается с + аргументом ≥ конфигурируемого значения. (Mock `asyncio.sleep`, + проверка count и аргументов.) +- **UT-WL-10.** `max_tracks_per_run`: при `max_tracks_per_run=2` и mock + поиске на ≥ 5 треков — `collect()` yield-ит ровно 2 трека. + +### REQ-F-12 — Integration-тест pipeline на mock-источниках + +Файл `tests/integration/test_pipeline_et009.py` (новый). + +Использует respx или httpx_mock для подмены HTTP. Запускает +`scripts/gps_collect.py:main` (через `asyncio.run`) с временной БД. + +- **IT-ER-01.** Pipeline с mock EnduroRussia (фикстурный JSON + + 3 GPX) + регион `tsfo_plus_chuvashia` → в БД 2 трека (третий + отфильтрован bbox-ом), `pipeline_runs[-1].status='ok'`, + `tracks_new=2`. +- **IT-WL-01.** Pipeline с mock Wikiloc (фикстурный HTML поиска + 1 + страница трека + 1 GPX) → в БД 1 трек, `pipeline_runs[-1].status='ok'`, + `tracks_new=1`. +- **IT-WL-02.** Mock Wikiloc возвращает 403 на странице поиска → + `pipeline_runs[-1].status='partial'` или `'rate_limited'`, + `tracks_new=0`, exit-code pipeline не 0 (есть error) **либо** + exit-code 0 при условии что graceful-stop не считается error — + выбрать одно поведение и зафиксировать тест на нём. **Решение:** + graceful-stop ≠ error, exit-code 0, status `'partial'`. +- **IT-DEDUP-01.** Pipeline сначала собирает EnduroRussia (1 трек), + затем Wikiloc (1 трек с теми же координатами и длиной ±5%, той же + датой ±1 день) → в БД одна запись с `sources=['enduro_russia','wikiloc']`, + `external_urls=[endurorussia.ru/…, wikiloc.com/…]`, метаданные + имеют приоритет `enduro_russia` (если `source_priority=80` выше + чем у wikiloc=70 — см. ET-008 dedup-merge). +- **IT-LIC-01.** Искусственно поменять `status: accepted` → + `status: proposed` в копии ADR-010 (через временный + `GPS_SOURCES_CONFIG` env с другим путём license_adr) → pipeline + пропускает source с `pipeline_runs[-1].status='skipped_license'`. + +### REQ-F-13 — Стили: цвета по источнику + +В файлах `src/web/style.json` и `src/web/style-dark.json` слой +`gps-tracks-layer` (или его эквивалент из ET-008) содержит +match-expression `line-color`: + +```json +[ + "match", + ["get", "source"], + "osm", "#3cb44b", + "enduro_russia", "#e6194b", + "wikiloc", "#4363d8", + "#808080" +] +``` + +Цвета — приближённо, окончательная палитра согласуется с UX в +момент реализации. Главное: для всех трёх известных источников +ID-→-цвет задан, fallback есть. + +Аналогично для `gps-tracks-halo-satellite` — halo всегда белый/ +полупрозрачный, цвет линии берётся тот же. + +### REQ-F-14 — Атрибуция + +После первого прогона, при наличии в БД треков из `enduro_russia`, +endpoint `/api/gps-tracks/health` возвращает в поле `attributions` +(если уже есть в ET-008) или в эквивалентном — список: +```json +["© OpenStreetMap contributors (ODbL)", "EnduroRussia.ru", "© Wikiloc contributors"] +``` + +Клиент `src/web/gps_tracks.js` подмешивает эти строки в MapLibre +attribution control (через `map.getControl(...)` или эквивалент). +Если в ET-008 атрибуция формируется на клиенте по статическому +маппингу `source_id → label` — расширить маппинг: +```js +const SOURCE_ATTRIBUTIONS = { + osm: "© OpenStreetMap contributors (ODbL)", + enduro_russia: "EnduroRussia.ru", + wikiloc: "© Wikiloc contributors", + ttrails: "ttrails.ru", +}; +``` + +### REQ-F-15 — Контрактный smoke-тест EnduroRussia API + +Файл `tests/contract/test_endurorussia_api_smoke.py` (новый, +помечается маркером `@pytest.mark.network` и не запускается в обычном +CI; запускается вручную или в nightly). + +- **CT-ER-01.** `GET https://endurorussia.ru/api/tracks?page=0&limit=5` + возвращает 200, JSON с ключами `items`, `total`. +- **CT-ER-02.** `GET https://endurorussia.ru/api/tracks/{first_id}/gpx` + возвращает 200, Content-Type содержит `xml` или `gpx`, тело + парсится `defusedxml` без exception. + +Назначение: при поломке внешнего API мы узнаём об этом из nightly, +а не из тишины health-эндпоинта. + +### REQ-F-16 — Контрактный smoke-тест Wikiloc (опционально) + +Из-за rate-limit и риска бана **не** делаем регулярный smoke-тест +Wikiloc. Вместо этого фиксируем в `docs/work-items/ET-009/13-test-report.md` +после первой ручной проверки факт того, что `find.do` отвечает 200 с +ожидаемой структурой. + +### REQ-F-17 — Первый продакшн-прогон + +После мерджа в main и деплоя в test-среду оператор запускает: + +```bash +ssh mva154 +cd /opt/enduro-trails +python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia +# (ждать ≈ 25 минут) +python scripts/gps_collect.py --region tsfo_plus_chuvashia --source wikiloc +# (ждать до достижения max_tracks_per_run, обычно 10-20 минут) +``` + +Результат фиксируется в `14-deploy-log.md`: +- `tracks_by_source.enduro_russia` (ожидаем ≥ 200); +- `tracks_by_source.wikiloc` (ожидаем ≥ 1); +- длительность каждого прогона; +- `errors_count` (ожидаем 0 или ≤ 5% от tracks_new). + +### REQ-F-18 — Не менять контракт `/api/gps-tracks` + +Endpoint `/api/gps-tracks` сохраняет интерфейс ET-008. Новые ID +источников (`enduro_russia`, `wikiloc`) появляются в значениях полей +ответа естественным образом; никаких новых query-параметров или +полей в FeatureCollection не вводится. + +### REQ-F-19 — Не менять алгоритм дедупликации + +`compute_dedup_key` в `dedup.py` не меняется. Никаких новых правил +для пары (enduro_russia, wikiloc) — стандартный +bbox+length+date-алгоритм должен справиться (см. ADR-006). + +### REQ-F-20 — Документация + +В `docs/work-items/ET-009/` должны существовать после Анализа: +- `00-business-request.md` (есть) +- `01-brd.md` (создаётся в ET-009) +- `02-trz.md` (этот файл) +- `03-acceptance-criteria.md` +- `04-test-plan.yaml` +- `04b-ui-test-cases.md` + +После реализации добавляются: `07-infra-requirements.md`, +`08-data-requirements.md`, `10-tech-risks.md`, `12-review.md`, +`13-test-report.md`, `14-deploy-log.md`. + +## 4. Не-функциональные требования + +### NFR-01 — Производительность сбора + +EnduroRussia: при `rate_limit_sec=5` и 305 треках полный прогон +региона `tsfo_plus_chuvashia` укладывается в ≤ 30 минут (305 × 5 +сек ≈ 25 мин + overhead). + +Wikiloc: первый прогон ограничен `max_tracks_per_run=50` → +максимум 50 × (1 search + 1 trail + 1 gpx) × 10 сек ≈ 25 минут. + +### NFR-02 — Стабильность + +Падение Wikiloc-парсера не должно валить весь pipeline. Покрывается +существующей логикой `scripts/gps_collect.py` (per-source error +не помечает остальные как error). + +### NFR-03 — Размер БД + +Прирост `data/gps_tracks.sqlite` после первого прогона ET-009: +≤ 100 MB при 200 треков EnduroRussia + 50 Wikiloc. Если фактический +прирост существенно больше — фиксируется в `14-deploy-log.md`. + +### NFR-04 — Логирование + +Pipeline и parser используют существующий `logger` стандартного +формата. Никаких новых форматов или sinks ET-009 не добавляет. + +### NFR-05 — Безопасность + +XML-парсинг GPX выполняется через `defusedxml.ElementTree` (как в +ET-008). Никаких изменений по security ET-009 не вносит. + +### NFR-06 — Совместимость + +Контракт `/api/gps-tracks*` не меняется. Существующие клиенты +(включая старые версии браузеров пользователей) продолжают работать +без обновления. + +## 5. План работ (для разработчика) + +1. **Сверка ADR-010 / ADR-012 → `status: accepted`** (REQ-F-05). Если нет — STOP. +2. **Правка `config/gps_sources.yaml`** (REQ-F-01, F-02, F-03). +3. **Правка `config/gps_regions.yaml`** (REQ-F-04). +4. **Снапшот реальных ответов API/HTML и сохранение как фикстуры** + (REQ-F-06..F-09). Снимки берутся **до** unit-тестов, чтобы тесты + опирались на реальные данные. +5. **Расширение Wikiloc-парсера `max_tracks_per_run`** (если ещё нет). +6. **Написание unit-тестов** (REQ-F-10, F-11). +7. **Написание integration-тестов** (REQ-F-12). +8. **Контрактный smoke-тест EnduroRussia** (REQ-F-15). +9. **Расширение стилей карты** (REQ-F-13). +10. **Атрибуция в клиенте** (REQ-F-14). +11. **Прогон всех тестов локально** (`make test`). +12. **Code review → merge → deploy в test**. +13. **Ручной первый прогон** (REQ-F-17). Запись в `14-deploy-log.md`. +14. **Проверка UI** по тест-плану `04b-ui-test-cases.md`. + +## 6. Открытые вопросы и решения по умолчанию + +| Вопрос | Решение по умолчанию | +| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Считать ли graceful-stop Wikiloc ошибкой? | **Нет.** `pipeline_runs.status='partial'`, exit-code 0. (См. IT-WL-02.) | +| Запускать ли cron автоматически после ET-009? | **Нет.** Cron включается отдельным DevOps-task'ом после двух успешных ручных прогонов подряд. | +| Маппить ли `wikiloc.act=motorcycle` (19) на `enduro` или `moto`? | **`moto`** (более широкая категория). MAPPING уже так сконфигурирован. | +| Что делать с старым URL `enduro-russia.ru` в external_url ранее собранных треков? | Опциональный one-shot `UPDATE`-скрипт; в ET-009 не обязателен (база test-среды чистая для практических целей). | +| Wikiloc возвращает `creator=Wikiloc` в GPX тех же треков, что и EnduroRussia? | **Нормально** — на это и нужен dedup-merge. | +| Нужно ли менять source_priority? | **Нет.** `osm=100`, `enduro_russia=80`, `wikiloc=70` — порядок задаёт приоритет метаданных при merge. | diff --git a/docs/work-items/ET-009/03-acceptance-criteria.md b/docs/work-items/ET-009/03-acceptance-criteria.md new file mode 100644 index 0000000..09c48d6 --- /dev/null +++ b/docs/work-items/ET-009/03-acceptance-criteria.md @@ -0,0 +1,218 @@ +--- +type: acceptance-criteria +work_item_id: ET-009 +title: "Acceptance Criteria: Новые источники GPS-треков — EnduroRussia и Wikiloc" +version: 1 +status: draft +created_at: 2026-06-01 +updated_at: 2026-06-01 +authors: + - "agent:analyst" +--- + +# Acceptance Criteria — ET-009 + +Критерии формализованы в Gherkin-стиле. Все критерии — обязательные; +задача считается принятой, когда **каждый** прошёл проверку в +test-среде или в автоматическом тестовом запуске CI. + +## AC-01 — Конфиг EnduroRussia исправлен и активирован + +**Given** запись `enduro_russia` в `config/gps_sources.yaml` +**When** работа ET-009 завершена +**Then**: +- `base_url` равно `https://endurorussia.ru` (без дефиса); +- `enabled` равно `true`; +- `license_adr` указывает на существующий файл с `status: accepted`; +- `rate_limit_sec` ≥ 5. + +## AC-02 — Конфиг Wikiloc добавлен + +**Given** `config/gps_sources.yaml` +**When** работа ET-009 завершена +**Then** существует запись с `id: wikiloc`, в которой: +- `enabled: true`; +- `base_url: https://www.wikiloc.com`; +- `rate_limit_sec: 10`; +- `license_adr: docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md`; +- `parser_module: src.api.gps_tracks.sources.wikiloc`; +- `save_user_field: false`; +- `attribution: "© Wikiloc contributors"`; +- задано `max_tracks_per_run` (любое целое > 0; для MVP — 50). + +## AC-03 — Wikiloc подписан на регион ЦФО+Чувашия + +**Given** `config/gps_regions.yaml` +**When** работа ET-009 завершена +**Then** запись `tsfo_plus_chuvashia.sources` содержит элемент `wikiloc`. +`enduro_russia` в этом списке уже был и остаётся. + +## AC-04 — Pipeline licensing-guard прозрачно работает + +**Given** `scripts/gps_collect.py` и ADR-010 со `status: accepted` +**When** оператор запускает `python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia` +**Then** в логах нет сообщения `skipped_license`, в `pipeline_runs` +последняя запись имеет `status` ∈ `{ok, partial}`, не `skipped_license`. + +**And given** искусственная подмена `status: accepted` на `status: proposed` в копии ADR-010 +**When** запуск pipeline с этим путём +**Then** `pipeline_runs[-1].status == 'skipped_license'`, exit-code 1. + +## AC-05 — Unit-тесты EnduroRussia зелёные + +**Given** ветка `feature/ET-009-…` с коммитом изменений +**When** CI запускает `pytest tests/unit/test_gps_tracks_enduro_russia.py -v` +**Then** все тесты UT-ER-01..UT-ER-08 проходят, exit-code 0. + +## AC-06 — Unit-тесты Wikiloc зелёные + +**Given** та же ветка +**When** CI запускает `pytest tests/unit/test_gps_tracks_wikiloc.py -v` +**Then** все тесты UT-WL-01..UT-WL-10 проходят, exit-code 0. + +## AC-07 — Integration-тесты pipeline зелёные + +**Given** ветка +**When** CI запускает `pytest tests/integration/test_pipeline_et009.py -v` +**Then** все тесты IT-ER-01, IT-WL-01, IT-WL-02, IT-DEDUP-01, IT-LIC-01 +проходят. + +## AC-08 — Тестовые фикстуры существуют в репо + +**Given** репо после слияния +**When** проверка файлов +**Then** следующие файлы существуют и не пустые: +- `tests/fixtures/gps-tracks/enduro-russia-api-tracks-page1.json` +- `tests/fixtures/gps-tracks/enduro-russia-track-1.gpx` (≥ 10 trkpt) +- `tests/fixtures/gps-tracks/enduro-russia-track-2.gpx` (пустой) +- `tests/fixtures/gps-tracks/enduro-russia-track-3.gpx` (вне bbox) +- `tests/fixtures/gps-tracks/wikiloc-search-page1.html` (≥ 5 ссылок на треки) +- `tests/fixtures/gps-tracks/wikiloc-trail-page.html` +- `tests/fixtures/gps-tracks/wikiloc-track.gpx` + +## AC-09 — Первый продакшн-прогон EnduroRussia + +**Given** mva154, ветка смерджена в main, deploy выполнен +**When** оператор выполняет +``` +python scripts/gps_collect.py --region tsfo_plus_chuvashia --source enduro_russia +``` +**Then**: +- exit-code 0; +- последняя запись `pipeline_runs` имеет `region_id='tsfo_plus_chuvashia'`, + `source_id='enduro_russia'`, `status='ok'` или `'partial'`; +- `tracks_new + tracks_updated ≥ 200`; +- `errors_json IS NULL` или содержит ≤ 5% от tracks_new; +- длительность ≤ 45 минут. + +## AC-10 — Первый продакшн-прогон Wikiloc + +**Given** mva154 и активированный `wikiloc` +**When** оператор выполняет +``` +python scripts/gps_collect.py --region tsfo_plus_chuvashia --source wikiloc +``` +**Then**: +- exit-code 0 (graceful-stop приемлем); +- последняя запись `pipeline_runs` имеет `status` ∈ `{ok, partial, rate_limited}`; +- `tracks_new + tracks_updated ≥ 1` (любое ненулевое — успех; ограничение `max_tracks_per_run=50`). + +## AC-11 — API возвращает новые источники + +**Given** БД после двух прогонов AC-09 + AC-10 +**When** клиент делает `GET /api/gps-tracks?bbox=37.0,55.0,38.0,56.0` +**Then** в ответе: +- статус 200; +- в `FeatureCollection.features[].properties.sources` встречаются строки + `"enduro_russia"` и/или `"wikiloc"` (для разных треков); +- ни одна feature не имеет в `sources` значение `"enduro-russia"` + (с дефисом) или другую опечатку. + +## AC-12 — Health-эндпоинт показывает новые источники + +**Given** БД после прогонов +**When** клиент делает `GET /api/gps-tracks/health` +**Then** в ответе: +- статус 200; +- поле `tracks_by_source` содержит ключи `enduro_russia` и `wikiloc` + с числовыми значениями ≥ 1. + +## AC-13 — Dedup-merge работает между источниками + +**Given** БД после прогонов +**When** SQL-запрос: +```sql +SELECT id, sources_json FROM tracks +WHERE sources_json LIKE '%enduro_russia%' + AND (sources_json LIKE '%wikiloc%' OR sources_json LIKE '%osm%'); +``` +**Then** возвращается ≥ 1 строка (хотя бы один трек попал в БД из ≥ 2 +источников и был объединён по dedup-key). + +**Note.** Если для данного снимка БД таких пересечений нет физически +(маловероятно при ≥ 200 треков EnduroRussia), AC-13 проверяется +синтетически через integration-тест IT-DEDUP-01 и считается покрытым. + +## AC-14 — Стили карты содержат цвета новых источников + +**Given** `src/web/style.json` и `src/web/style-dark.json` +**When** работа ET-009 завершена +**Then** в `paint.line-color` слоя для публичных треков (имя слоя по +ET-008 — `gps-tracks-layer` или эквивалент) match-expression +содержит ключи `osm`, `enduro_russia`, `wikiloc` с присвоенными цветами, +и есть fallback-значение по умолчанию. + +## AC-15 — Атрибуция отображается в UI + +**Given** в БД есть треки из всех трёх источников +**When** пользователь открывает страницу, включает «Публичные треки», +ждёт 3 сек +**Then** в строке атрибуции MapLibre (правый нижний угол) видны: +- «© OpenStreetMap contributors (ODbL)»; +- «EnduroRussia.ru»; +- «© Wikiloc contributors». + +## AC-16 — UI-фильтр источников показывает 3 чекбокса + +**Given** в БД есть треки трёх источников +**When** пользователь открывает `#sheet-gps-filters` +**Then** в секции «ИСТОЧНИК» (`#gps-source-grid`) видны минимум три +чекбокса с подписями «OSM», «EnduroRussia», «Wikiloc». По умолчанию +все установлены. + +## AC-17 — Снятие галки источника убирает соответствующие линии + +**Given** включён слой и видны треки трёх источников +**When** пользователь снимает галку «EnduroRussia» в фильтре +**Then** через ≤ 200 мс на карте все линии цвета `enduro_russia` (или +все треки с этим источником в `properties.sources`) исчезают; OSM и +Wikiloc остаются. + +## AC-18 — Документация work item полная + +**Given** репо после слияния ET-009 +**When** проверка `docs/work-items/ET-009/` +**Then** существуют: +- `00-business-request.md` +- `01-brd.md` +- `02-trz.md` +- `03-acceptance-criteria.md` +- `04-test-plan.yaml` +- `04b-ui-test-cases.md` +- `13-test-report.md` (после Тестирования) +- `14-deploy-log.md` (после Деплоя) + +## AC-19 — Регрессия ET-008 не сломана + +**Given** все существующие e2e-тесты ET-008 +**When** CI прогоняет `pytest tests/e2e/ -v` (или соответствующий +маркер) +**Then** все тесты ET-008 (E-01..E-41 из `docs/work-items/ET-008/04-test-plan.yaml`) +проходят без регрессий, как и до ET-009. + +## AC-20 — Производительность endpoint не деградировала + +**Given** БД с треками после ET-009 (новые источники добавлены) +**When** нагрузочный тест 100 запросов `GET /api/gps-tracks?bbox=…` на +z=10 с 500 треков в bbox +**Then** p95 latency ≤ 300 мс (не выше, чем baseline ET-008). diff --git a/docs/work-items/ET-009/04-test-plan.yaml b/docs/work-items/ET-009/04-test-plan.yaml new file mode 100644 index 0000000..3646416 --- /dev/null +++ b/docs/work-items/ET-009/04-test-plan.yaml @@ -0,0 +1,432 @@ +--- +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/', + 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//'" + + - 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:

" + input: "HTML с '

Test Trail

'" + 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" +--- diff --git a/docs/work-items/ET-009/04b-ui-test-cases.md b/docs/work-items/ET-009/04b-ui-test-cases.md new file mode 100644 index 0000000..f5de734 --- /dev/null +++ b/docs/work-items/ET-009/04b-ui-test-cases.md @@ -0,0 +1,302 @@ +--- +type: ui-test-cases +work_item_id: ET-009 +title: "UI Test Cases: Новые источники GPS-треков на карте" +version: 1 +status: draft +created_at: 2026-06-01 +updated_at: 2026-06-01 +authors: + - "agent:analyst" +related: + - "ET-008" +--- + +# UI Test Cases — ET-009: Новые источники GPS-треков на карте + +Базовый URL: `https://openclaw.mva154.duckdns.org/enduro/` + +ET-009 не добавляет новых UI-компонентов. Все селекторы и поведение +взяты из ET-008 (`docs/work-items/ET-008/04b-ui-test-cases.md`). +Цель тест-кейсов — проверить, что **новые ID источников +(`enduro_russia`, `wikiloc`)** корректно появляются в существующих +UI-фикстурах: фильтр источников, атрибуция, цветовая палитра, popup, +ссылки на оригинал. + +Селекторы (унаследованы из ET-008): + +- `#terrain-toggle` — кнопка попапа слоёв. +- `#public-tracks-cb` — чекбокс «Публичные треки» в `#terrain-popup`. +- `#public-tracks-filters-btn` — ссылка «Фильтры…». +- `#sheet-gps-filters` — bottom sheet фильтров. +- `#gps-source-grid` — секция чекбоксов источников. +- `#gps-source-grid input[value='enduro_russia']` — чекбокс EnduroRussia. +- `#gps-source-grid input[value='wikiloc']` — чекбокс Wikiloc. +- `#gps-source-grid input[value='osm']` — чекбокс OSM. +- `#gps-color-by-source`, `#gps-color-by-activity` — color-mode. +- `.gps-track-popup` — popup трека. +- `#base-btn-satellite` — переключение на спутник. +- `#btn-theme` — переключение тёмной темы. +- `#map` — карта. + +Предусловие для всех тестов: в БД test-среды есть треки всех трёх +источников. Это достигается ручным прогоном (E2E-PROD-01 / E2E-PROD-02 +из test-plan) перед запуском UI-тестов; либо mock-backend подменяет +`/api/gps-tracks*` фикстурами c треками `enduro_russia` и `wikiloc`. + +--- + +### TC-UI-ER-01 — Чекбокс EnduroRussia виден в фильтре источников + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. screenshot: "et009-01-source-filter-enduro-russia" +10. check-visual: "В bottom-sheet #sheet-gps-filters в секции «ИСТОЧНИК» видны минимум три чекбокса с подписями (например): «OSM», «EnduroRussia», «Wikiloc». Чекбокс «EnduroRussia» имеет селектор #gps-source-grid input[value='enduro_russia'] и установлен по умолчанию." + +--- + +### TC-UI-WL-01 — Чекбокс Wikiloc виден в фильтре источников + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. screenshot: "et009-02-source-filter-wikiloc" +10. check-visual: "В секции «ИСТОЧНИК» виден чекбокс с подписью «Wikiloc», селектор #gps-source-grid input[value='wikiloc']. Установлен по умолчанию." + +--- + +### TC-UI-ER-02 — Снятие галки EnduroRussia скрывает соответствующие линии + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. screenshot: "et009-03a-all-sources-visible" +8. check-visual: "На карте видны линии трёх цветов (OSM, EnduroRussia, Wikiloc). Можно различить минимум два разных цвета." +9. click: "#public-tracks-filters-btn" +10. wait: 800 +11. click: "#gps-source-grid input[value='enduro_russia']" +12. wait: 500 +13. screenshot: "et009-03b-enduro-russia-hidden" +14. check-visual: "Чекбокс EnduroRussia снят. На карте линии цвета EnduroRussia (по умолчанию match-expression задаёт характерный цвет, например красный) исчезли. OSM и Wikiloc-линии остались. Счётчик «Видны» в нижней части sheet уменьшился." + +--- + +### TC-UI-WL-02 — Снятие галки Wikiloc скрывает соответствующие линии + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. click: "#gps-source-grid input[value='wikiloc']" +10. wait: 500 +11. screenshot: "et009-04-wikiloc-hidden" +12. check-visual: "Чекбокс Wikiloc снят. На карте линии цвета Wikiloc исчезли, OSM и EnduroRussia-линии остаются. Счётчик «Видны» уменьшился." + +--- + +### TC-UI-ER-03 — Popup трека EnduroRussia содержит правильный URL + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. click: "#gps-source-grid input[value='osm']" +10. wait: 300 +11. click: "#gps-source-grid input[value='wikiloc']" +12. wait: 500 +13. check-visual: "На карте видны только треки EnduroRussia." +14. click: "#map" +15. wait: 1500 +16. screenshot: "et009-05-popup-enduro-russia" +17. check-visual: "Открылся popup .gps-track-popup. В списке источников содержится «EnduroRussia» (или эквивалентная подпись). Ссылка '↗' указывает на https://endurorussia.ru/tracks/ (БЕЗ дефиса в домене). Hover/click на ссылку открывает endurorussia.ru, не enduro-russia.ru." + +--- + +### TC-UI-WL-03 — Popup трека Wikiloc содержит правильный URL + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. click: "#gps-source-grid input[value='osm']" +10. wait: 300 +11. click: "#gps-source-grid input[value='enduro_russia']" +12. wait: 500 +13. check-visual: "На карте видны только треки Wikiloc." +14. click: "#map" +15. wait: 1500 +16. screenshot: "et009-06-popup-wikiloc" +17. check-visual: "Открылся popup. В списке источников содержится «Wikiloc». Ссылка '↗' указывает на https://www.wikiloc.com/...." + +--- + +### TC-UI-ATTR-01 — Атрибуция содержит EnduroRussia.ru и Wikiloc + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 4000 +7. screenshot: "et009-07-attribution" +8. check-visual: "В правом нижнем углу карты в стандартной MapLibre-панели атрибуции (либо после клика на иконку 'i') видны строки: «© OpenStreetMap contributors (ODbL)», «EnduroRussia.ru», «© Wikiloc contributors». Текст «EnduroRussia.ru» написан БЕЗ дефиса." + +--- + +### TC-UI-COLOR-01 — Color-by-source: три разных цвета линий + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. click: "#gps-color-by-source" +10. wait: 500 +11. screenshot: "et009-08-color-by-source-three" +12. check-visual: "Активен переключатель «По источнику». На карте видны минимум 3 различимых цвета линий (OSM — один, EnduroRussia — другой, Wikiloc — третий). Серый fallback не должен преобладать (если он используется, значит цвета для конкретных источников не заданы — это баг по AC-14)." + +--- + +### TC-UI-SAT-01 — Halo на спутнике для треков EnduroRussia и Wikiloc + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#base-btn-satellite" +8. wait: 5000 +9. screenshot: "et009-09-public-tracks-on-satellite" +10. check-visual: "На спутниковой подложке видны линии всех трёх источников (OSM, EnduroRussia, Wikiloc), у каждой есть белая обводка-halo. Линии Wikiloc/EnduroRussia читаемы на тёмном фоне снимков." + +--- + +### TC-UI-PROD-01 — После прогона EnduroRussia на test-среде — треки появились + +- тип: ui +- viewport: desktop +- условие: запускается после E2E-PROD-01 ручного прогона + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 4000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. click: "#gps-source-grid input[value='osm']" +10. wait: 300 +11. click: "#gps-source-grid input[value='wikiloc']" +12. wait: 500 +13. screenshot: "et009-10-only-enduro-russia-real-data" +14. check-visual: "На карте видны линии исключительно EnduroRussia (200+ треков по ЦФО). Линии хорошо распределены по территории ЦФО и Чувашии." + +--- + +### TC-UI-MOBILE-01 — Фильтр на мобильном: три источника + +- тип: ui +- viewport: mobile + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. click: "#public-tracks-cb" +6. wait: 3000 +7. click: "#public-tracks-filters-btn" +8. wait: 800 +9. screenshot: "et009-11-source-filter-mobile" +10. check-visual: "На мобильном viewport bottom-sheet #sheet-gps-filters занимает всю ширину. В секции «ИСТОЧНИК» помещаются минимум 3 чекбокса (OSM, EnduroRussia, Wikiloc), все нажимаемы (44×44 dp), подписи не обрезаются." + +--- + +### TC-UI-REGRESS-01 — Регрессия: чекбокс «Публичные треки» работает как в ET-008 + +- тип: ui +- viewport: desktop + +шаги: +1. navigate: https://openclaw.mva154.duckdns.org/enduro/ +2. wait: 5000 +3. click: "#terrain-toggle" +4. wait: 500 +5. screenshot: "et009-12-regress-popup-with-checkbox" +6. check-visual: "В попапе #terrain-popup видна строка «Публичные треки» с чекбоксом #public-tracks-cb. По умолчанию чекбокс снят. Поведение идентично ET-008 TC-UI-01." +7. click: "#public-tracks-cb" +8. wait: 3000 +9. screenshot: "et009-13-regress-checkbox-on" +10. check-visual: "Линии публичных треков отрисовались. Поведение идентично ET-008 TC-UI-02." +11. click: "#public-tracks-cb" +12. wait: 1500 +13. screenshot: "et009-14-regress-checkbox-off" +14. check-visual: "Линии исчезли. Поведение идентично ET-008 TC-UI-20."