Тесты разбора площадок: дом Авито, выдачи Циана и Яндекса снова проверяются на сохранённых страницах #3580

Merged
bot-backend merged 2 commits from fix/kit-parser-unit-tests into main 2026-09-17 11:23:33 +00:00
Collaborator

#2422: тесты kit-парсеров на сохранённых страницах (поглотил закрытый дубль #2408)

Что было

Golden-parity тесты сверяли kit-парсеры с legacy-копией и были удалены вместе с ней (#2419). Состояние на origin/main 85d455e3:

  • avito.houses: parse_houses_state, _parse_house_page, _parse_reviews, _parse_development_page не вызывал ни один тест. Проверялись только _normalize_house_type и реэкспорт _unix_to_date.
  • cian.serp: _parse_serp_html вызывался только на заглушках вида <html>irrelevant</html> с подменённым state (test_cian_serp_offers_total_mismatch.py, test_captcha_vs_empty_detect.py). Поля лота не проверялись.
  • yandex.serp: _entity_to_lot и _parse_gate_json не встречались ни в одном тесте.
  • avito.serp: разбор заголовка (комнаты, площадь, этаж) и цены на фикстуре не проверялся. _extract_rooms/area/floor_from_title не упоминались в тестах.
  • guard «нет app.* в kit»: был только у scheduler и contracts. Общий тест удалили вместе с test_kit_avito_has_no_app_imports.
  • Фикстуры без единой ссылки: avito_house_572538.html, avito_houses_sample.html, avito_novostroyka_12879380.html. cian_flat_330982715.html и domclick_bff_offers_sample.json к этому моменту уже использовались.
  • avito.detail, cian.detail, domclick и base уже покрыты на фикстурах в своих файлах: test_avito_detail_fields_3047.py, test_cian_views_parse.py, test_cian_bti_house_persist.py, test_domclick_sweep.py, scrapers/test_domclick_detail.py, scrapers/test_base_clamp_date.py. Их я не дублировал.

Чем это опасно: правка извлечения поля в kit (рейтинг, цена, адрес, канонический url) проходила зелёной.

Что сделано

Изменения только в тестах. Код kit не менялся.

tests/scrapers/test_2422_kit_parsers_on_fixtures.py (19 тестов). Ожидания записаны значениями из самих страниц, второй реализации в роли эталона нет. Значения для Циана и Яндекса я сверил с сырыми полями фикстур отдельным скриптом, мимо парсера.

  • avito.houses, дома 572538 и 115188:
    • характеристики дома;
    • рейтинг из reviews.filterEntries (#1789). В обеих карточках бейдж housePage приходит без оценки, так что без запасного пути рейтинг был бы None;
    • отзывы с разбором секций и дат;
    • reviews_next_page_url;
    • история размещений: цены, даты по МСК, removed_date=None;
    • мини-выдача: цена из строки с неразрывными пробелами, продавец;
    • рекомендации.
  • avito.houses, ЖК 12879380: ветка developmentPage. Проверены класс, тип дома, лифты, застройщик, пустой fullAddress, превращённый в None, и отсутствие рейтинга и года постройки.
  • avito.serp (avito_serp_sample.html): цена, комнаты, площадь, этаж и этажность, дата из sortTimeStamp.
  • cian.serp: новая фикстура cian_serp_ekb_2room_trimmed.html. Это живая выдача от 2026-09-17, урезанная до 4 offers из 28 и до ключей, которые читает парсер.
    • Телефоны, user и userId убраны.
    • Разбор урезанной и полной выдачи совпал поле в поле, кроме числа фото.
    • Проверяются вторичка, новостройка с домом (год сдачи из deadline), decoration=fine с балконом, новостройка без улицы. На последней номер дома не выдумывается (#1773).
    • Отдельно проверяются totalOffers=13895 и счётчики извлечения state.
  • yandex.serp: новые фикстуры yandex_gate_ekb_{vtorichka,novostroyki}_trimmed.json. Это живой gate-API от 2026-09-17: 3+1 entity, у author убран agentName.
    • Проверяются канонизация партнёрского url (#2235), агентство и собственник, прогнозная цена и тренд, студия без roomsTotal, лимит в 5 фото, новостройка без оценки и потолка.
    • Отдельно проверяется pager.

tests/scrapers/test_2422_kit_no_app_imports.py. Тест обходит весь scraper_kit, а не только providers, и через AST ищет import app / from app (включая ленивые импорты внутри функций). Чтобы тест не был зелёным по построению, он сначала проверяет, что обход дошёл до providers/avito/houses.py и нашёл больше 40 файлов.

Почему тесты лежат не в packages/scraper-kit/tests: CI (ci-tradein.yml) запускает pytest только из tradein-mvp/backend. Каталог внутри пакета никто бы не собирал. Для проверки: cd tradein-mvp/packages/scraper-kit && uv run python -m pytest -q выдаёт «no tests ran», rc=5. Это тот же класс дефекта, что и каталог вне пайплайна в #2722.

Судьба осиротевших фикстур: все три используются как вход новых тестов, удалять нечего.

Тесты

  • Новые файлы: 20 passed, rc=0.
  • Весь сьют после rebase на origin/main 85d455e3: DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider дал 6394 passed, 44 skipped, rc=0.
  • uv run ruff check app tests: All checks passed. ruff format --check по новым файлам чистый.

Фальсификация

Мутационный прогон: копия исходника в scratchpad, правка одной строки в kit, pytest новых файлов (rc берётся у самого pytest), восстановление, затем diff -q против копии (все файлы идентичны). Каждая из 9 порч роняет хотя бы один тест:

M1 houses: запасной путь #1789 ищет "scores"      rc=1  E assert None == 4.666666666666667 ± 4.7e-06
M2 houses: developmentPage под чужим ключом        rc=1  E assert 0 == 12879380
M3 houses: start_price из lastPrice                rc=1  E assert (14000000, 14000000) == (11000000, 14000000)
M4 avito.serp: этаж и этажность переставлены       rc=1  E assert [('8043936560... 38.0, 16, 3)] == [('8043936560... 38.0, 3, 16)]
M5 cian.serp: год сдачи из deadline не берётся     rc=1  2 failed: E assert None == 2028 / E assert None == 2026
M6 cian.serp: метро не выкидывается из адреса      rc=1  3 failed (address)
M7 yandex.serp: партнёрский url не канонизируется  rc=1  E assert 'https://novo...a/object/1064' == 'https://real...943217465492/'
M8 yandex.serp: студия получает rooms=None         rc=1  E assert None == 0
M9 guard: import app.core.config внутри _format_address  rc=1  E assert {'providers/c...трока 1254)']} == {}

Замечено по ходу (не чинил, в тестах не закрепил)

  1. Отзывы Авито с датой без года теряют дату. Пример: «16 апреля» для отзывов текущего года. _parse_ru_date требует год и возвращает None. Прод, чтение 2026-09-17: в house_reviews у 69 из 188 строк rated_date IS NULL, и у всех 69 в raw_payload->>'rated' нет года. Для починки нужно выбрать правило вывода года и сделать backfill строк: правка разбора сама старые строки не перезапишет.
  2. Адрес в выдаче Циана теряет части типа location после района. Offer 333611840: geo.address содержит «Юго-Западный жилрайон» и «ФорумПарк ЖК» с type=location. _format_address выкидывает все location (задумано для области и города), и получается «р-н Верх-Исетский, 3».
  3. Балкон у Циана. При balconiesCount=0 и loggiasCount=1 получается has_balcony=False: лоджия учитывается только при balconiesCount=None (offer 331661795). Неясно, задумано ли так.

Приёмка на проде

Изменения только в тестах: backend/tests/ в .dockerignore, поведение на проде не меняется. Приёмка на проде: после мержа, не позже 2026-09-18, pre-merge гейт ci-tradein и post-merge test-job в deploy-tradein должны быть зелёными на коммите мержа, а в логе pytest должны быть оба файла tests/scrapers/test_2422_*.

Деплой: правка под tradein-mvp/backend/** включает фильтр backend, пересобирает backend-образ и пересоздаёт tradein-backend и tradein-scraper с drain до 5 минут. Перед мержем нужно проверить SELECT count(*) FROM scrape_runs WHERE status='running' (задача «деплой убивает работающее»). Миграций нет.

Closes #2422

🤖 Generated with Claude Code

## #2422: тесты kit-парсеров на сохранённых страницах (поглотил закрытый дубль #2408) ### Что было Golden-parity тесты сверяли kit-парсеры с legacy-копией и были удалены вместе с ней (#2419). Состояние на origin/main 85d455e3: - **avito.houses**: `parse_houses_state`, `_parse_house_page`, `_parse_reviews`, `_parse_development_page` не вызывал ни один тест. Проверялись только `_normalize_house_type` и реэкспорт `_unix_to_date`. - **cian.serp**: `_parse_serp_html` вызывался только на заглушках вида `<html>irrelevant</html>` с подменённым state (`test_cian_serp_offers_total_mismatch.py`, `test_captcha_vs_empty_detect.py`). Поля лота не проверялись. - **yandex.serp**: `_entity_to_lot` и `_parse_gate_json` не встречались ни в одном тесте. - **avito.serp**: разбор заголовка (комнаты, площадь, этаж) и цены на фикстуре не проверялся. `_extract_rooms/area/floor_from_title` не упоминались в тестах. - **guard «нет app.\* в kit»**: был только у scheduler и contracts. Общий тест удалили вместе с `test_kit_avito_has_no_app_imports`. - **Фикстуры без единой ссылки**: `avito_house_572538.html`, `avito_houses_sample.html`, `avito_novostroyka_12879380.html`. `cian_flat_330982715.html` и `domclick_bff_offers_sample.json` к этому моменту уже использовались. - avito.detail, cian.detail, domclick и base уже покрыты на фикстурах в своих файлах: `test_avito_detail_fields_3047.py`, `test_cian_views_parse.py`, `test_cian_bti_house_persist.py`, `test_domclick_sweep.py`, `scrapers/test_domclick_detail.py`, `scrapers/test_base_clamp_date.py`. Их я не дублировал. Чем это опасно: правка извлечения поля в kit (рейтинг, цена, адрес, канонический url) проходила зелёной. ### Что сделано Изменения только в тестах. Код kit не менялся. **`tests/scrapers/test_2422_kit_parsers_on_fixtures.py`** (19 тестов). Ожидания записаны значениями из самих страниц, второй реализации в роли эталона нет. Значения для Циана и Яндекса я сверил с сырыми полями фикстур отдельным скриптом, мимо парсера. - **avito.houses**, дома 572538 и 115188: - характеристики дома; - рейтинг из `reviews.filterEntries` (#1789). В обеих карточках бейдж housePage приходит без оценки, так что без запасного пути рейтинг был бы None; - отзывы с разбором секций и дат; - `reviews_next_page_url`; - история размещений: цены, даты по МСК, `removed_date=None`; - мини-выдача: цена из строки с неразрывными пробелами, продавец; - рекомендации. - **avito.houses**, ЖК 12879380: ветка `developmentPage`. Проверены класс, тип дома, лифты, застройщик, пустой `fullAddress`, превращённый в None, и отсутствие рейтинга и года постройки. - **avito.serp** (`avito_serp_sample.html`): цена, комнаты, площадь, этаж и этажность, дата из `sortTimeStamp`. - **cian.serp**: новая фикстура `cian_serp_ekb_2room_trimmed.html`. Это живая выдача от 2026-09-17, урезанная до 4 offers из 28 и до ключей, которые читает парсер. - Телефоны, `user` и `userId` убраны. - Разбор урезанной и полной выдачи совпал поле в поле, кроме числа фото. - Проверяются вторичка, новостройка с домом (год сдачи из `deadline`), `decoration=fine` с балконом, новостройка без улицы. На последней номер дома не выдумывается (#1773). - Отдельно проверяются `totalOffers=13895` и счётчики извлечения state. - **yandex.serp**: новые фикстуры `yandex_gate_ekb_{vtorichka,novostroyki}_trimmed.json`. Это живой gate-API от 2026-09-17: 3+1 entity, у author убран `agentName`. - Проверяются канонизация партнёрского url (#2235), агентство и собственник, прогнозная цена и тренд, студия без `roomsTotal`, лимит в 5 фото, новостройка без оценки и потолка. - Отдельно проверяется pager. **`tests/scrapers/test_2422_kit_no_app_imports.py`**. Тест обходит весь `scraper_kit`, а не только providers, и через AST ищет `import app` / `from app` (включая ленивые импорты внутри функций). Чтобы тест не был зелёным по построению, он сначала проверяет, что обход дошёл до `providers/avito/houses.py` и нашёл больше 40 файлов. **Почему тесты лежат не в `packages/scraper-kit/tests`**: CI (`ci-tradein.yml`) запускает pytest только из `tradein-mvp/backend`. Каталог внутри пакета никто бы не собирал. Для проверки: `cd tradein-mvp/packages/scraper-kit && uv run python -m pytest -q` выдаёт «no tests ran», rc=5. Это тот же класс дефекта, что и каталог вне пайплайна в #2722. **Судьба осиротевших фикстур**: все три используются как вход новых тестов, удалять нечего. ### Тесты - Новые файлы: `20 passed`, rc=0. - Весь сьют после rebase на origin/main 85d455e3: `DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider` дал `6394 passed, 44 skipped`, rc=0. - `uv run ruff check app tests`: All checks passed. `ruff format --check` по новым файлам чистый. ### Фальсификация Мутационный прогон: копия исходника в scratchpad, правка одной строки в kit, pytest новых файлов (rc берётся у самого pytest), восстановление, затем `diff -q` против копии (все файлы идентичны). Каждая из 9 порч роняет хотя бы один тест: ``` M1 houses: запасной путь #1789 ищет "scores" rc=1 E assert None == 4.666666666666667 ± 4.7e-06 M2 houses: developmentPage под чужим ключом rc=1 E assert 0 == 12879380 M3 houses: start_price из lastPrice rc=1 E assert (14000000, 14000000) == (11000000, 14000000) M4 avito.serp: этаж и этажность переставлены rc=1 E assert [('8043936560... 38.0, 16, 3)] == [('8043936560... 38.0, 3, 16)] M5 cian.serp: год сдачи из deadline не берётся rc=1 2 failed: E assert None == 2028 / E assert None == 2026 M6 cian.serp: метро не выкидывается из адреса rc=1 3 failed (address) M7 yandex.serp: партнёрский url не канонизируется rc=1 E assert 'https://novo...a/object/1064' == 'https://real...943217465492/' M8 yandex.serp: студия получает rooms=None rc=1 E assert None == 0 M9 guard: import app.core.config внутри _format_address rc=1 E assert {'providers/c...трока 1254)']} == {} ``` ### Замечено по ходу (не чинил, в тестах не закрепил) 1. **Отзывы Авито с датой без года теряют дату.** Пример: «16 апреля» для отзывов текущего года. `_parse_ru_date` требует год и возвращает None. Прод, чтение 2026-09-17: в `house_reviews` у 69 из 188 строк `rated_date IS NULL`, и у всех 69 в `raw_payload->>'rated'` нет года. Для починки нужно выбрать правило вывода года и сделать backfill строк: правка разбора сама старые строки не перезапишет. 2. **Адрес в выдаче Циана теряет части типа `location` после района.** Offer 333611840: `geo.address` содержит «Юго-Западный жилрайон» и «ФорумПарк ЖК» с `type=location`. `_format_address` выкидывает все `location` (задумано для области и города), и получается «р-н Верх-Исетский, 3». 3. **Балкон у Циана.** При `balconiesCount=0` и `loggiasCount=1` получается `has_balcony=False`: лоджия учитывается только при `balconiesCount=None` (offer 331661795). Неясно, задумано ли так. ### Приёмка на проде Изменения только в тестах: `backend/tests/` в `.dockerignore`, поведение на проде не меняется. Приёмка на проде: после мержа, не позже 2026-09-18, pre-merge гейт `ci-tradein` и post-merge test-job в `deploy-tradein` должны быть зелёными на коммите мержа, а в логе pytest должны быть оба файла `tests/scrapers/test_2422_*`. **Деплой**: правка под `tradein-mvp/backend/**` включает фильтр `backend`, пересобирает backend-образ и пересоздаёт tradein-backend и tradein-scraper с drain до 5 минут. Перед мержем нужно проверить `SELECT count(*) FROM scrape_runs WHERE status='running'` (задача «деплой убивает работающее»). Миграций нет. Closes #2422 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 10:06:22 +00:00
Тесты kit-парсеров на сохранённых страницах: дом Авито, выдачи Циана и Яндекса (#2422, #2408)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 32s
CI / changes (pull_request) Successful in 33s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 8m7s
6003ddedad
После удаления golden-parity (#2419) разбор avito.houses, cian.serp и yandex.serp
не проверял ни один тест: правка извлечения поля в kit проходила бы зелёной.

- avito.houses: обе осиротевшие карточки дома (572538, 115188) — характеристики,
  рейтинг из reviews.filterEntries (#1789), отзывы, история размещений, мини-выдача
  с продавцом, рекомендации; карточка ЖК 12879380 — ветка developmentPage.
- avito.serp: цена, комнаты, площадь, этаж/этажность и дата из sortTimeStamp
  на avito_serp_sample.html.
- cian.serp: живая выдача 2026-09-17, урезанная до 4 offers и читаемых ключей
  (без телефонов и данных пользователя): вторичка, новостройка с домом, отделка
  и балкон, новостройка без улицы — номер дома не выдумывается (#1773).
- yandex.serp: живой gate-API 2026-09-17: канонизация партнёрского url (#2235),
  собственник/агентство, студия без roomsTotal, новостройка без оценки.
- guard: ни один модуль scraper_kit не импортирует app.* (AST, включая ленивые
  импорты) — замена удалённого test_kit_avito_has_no_app_imports.

Тесты лежат в tradein-mvp/backend/tests/scrapers, а не в packages/scraper-kit/tests:
CI гоняет pytest только из backend, каталог в пакете не собирался бы никем.

Мутационный прогон: 9 порч kit (рейтинг #1789, developmentPage, цена истории,
этаж Авито, год сдачи и адрес Циана, url и студия Яндекса, import app) — каждая
роняет хотя бы один тест.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Light1YT added 1 commit 2026-09-17 10:50:39 +00:00
Merge remote-tracking branch 'origin/main' into fix/kit-parser-unit-tests
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 27s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 33s
CI Trade-In / backend-tests (pull_request) Successful in 7m9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
5af29ee9a4
bot-backend merged commit 7da94f183f into main 2026-09-17 11:23:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3580
No description provided.