feat(ET-009): analyst artifacts — BRD, TRZ, AC, test plan
Some checks failed
CI / lint (push) Failing after 4s
CI / test (push) Failing after 5s
CI / build (push) Has been skipped

This commit is contained in:
2026-06-01 17:51:47 +00:00
parent 9d7e5cd7e8
commit eaa6b4cd27
5 changed files with 1643 additions and 0 deletions

View File

@@ -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=<N>&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=<code>&sw=<lat,lon>&ne=<lat,lon>&page=<N>` → HTML с `<a href="/trails/…/<id>">` |
| Endpoint трека | `GET /trails/<slug>/<id>` → HTML c ссылкой на GPX |
| Endpoint GPX | `GET /wikiloc/downloadTrail.do?id=<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` строки 3773): при `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.

View File

@@ -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/<source_id>.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` (строки 3773) **не
изменяется**. Перед активацией 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. Один из них должен:
- содержать `<trk><trkseg><trkpt>` с ≥ 10 точками;
- лежать в bbox региона `tsfo_plus_chuvashia` (29..47.5 longitude,
49.5..60.0 latitude);
- иметь creator или metadata, идентифицирующее источник.
Второй GPX должен быть пустой (`<trkseg></trkseg>`) или с 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/<slug>/<id>`.
### REQ-F-09 — Тест-фикстуры Wikiloc страницы трека и GPX
- `tests/fixtures/gps-tracks/wikiloc-trail-page.html` — снимок
страницы одного трека Wikiloc; должен содержать `<h1>` с
названием и либо прямую ссылку на `.gpx`, либо
`downloadTrail.do?id=<id>`.
- `tests/fixtures/gps-tracks/wikiloc-track.gpx` — реальный GPX,
возвращаемый `/wikiloc/downloadTrail.do?id=<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/<id>"`.
- **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=<track_id>`.
- **UT-WL-04.** `_extract_track_name` извлекает текст `<h1>`.
- **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. |

View File

@@ -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).

View File

@@ -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/<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"
---

View File

@@ -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/<id> (БЕЗ дефиса в домене). 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."