"""Отрицательные формы не дают ОБРАТНЫЙ статус продажи (#2464). `_STATUS_KW_RE` искал ключевые слова без защиты от отрицания, поэтому русские отрицательные формы классифицировались наоборот: «нереализована» → содержит «реализована» → sold «не продана» → содержит «продана» → sold «не забронирована» → содержит «забронирована» → reserved «не в продаже» → содержит «в продаже» → free То есть свободная квартира могла попасть в базу проданной. Отрицание теперь гасит токен, а не переворачивает его: «не забронирована» не значит ни sold, ни free, а «не продана → free» — вывод, а не факт со страницы. Лучше отсутствие статуса, чем неверный. Тесты идут через `parse_catalog_flat` (полный путь разбора HTML), а не только через хелпер: инвертированный статус — это то, что доехало бы до БД. Текущий эффект на проде — НУЛЕВОЙ, и это проверено, а не предположено: `catalog_updated_at` пуст у всех 983 088 строк `domrf_kn_flats`, таска `scrape_kn_catalog_flats` закомментирована в beat_schedule.py (WAF-cooldown). Правка предупредительная: при включении пути дефект инвертировал бы статусы молча. """ from __future__ import annotations import pytest from app.services.scrapers.domrf_catalog import ( STATUS_FREE, STATUS_RESERVED, STATUS_SOLD, parse_catalog_flat, ) def _html_with_badge(badge_text: str, nav_text: str = "") -> str: """Тот же каркас, что в test_domrf_catalog_parse.py: nav + статус-бейдж.""" return ( "
" f'' f'Площадь 45 м²
" "" ) @pytest.mark.parametrize( "badge,было_бы", [ ("Нереализована", STATUS_SOLD), ("Не реализована", STATUS_SOLD), ("Не продана", STATUS_SOLD), ("Квартира не продана", STATUS_SOLD), ("Не забронирована", STATUS_RESERVED), ("Не в продаже", STATUS_FREE), ], ) def test_negated_form_does_not_invert_status(badge: str, было_бы: str) -> None: """Головной: отрицание не должно давать статус, тем более противоположный. На origin/main каждый случай возвращает `было_бы` — ровно обратное значение. """ result = parse_catalog_flat(_html_with_badge(badge)) got = result.get("status") assert got != было_бы, ( f"«{badge}» классифицировано как {got!r} — отрицание перевёрнуто в свою " "противоположность и уехало бы в domrf_kn_flats.status" ) assert got is None, f"«{badge}» дало статус {got!r}; ожидали отсутствие статуса" @pytest.mark.parametrize( "badge,ожидаем", [ ("Продана", STATUS_SOLD), ("Продано", STATUS_SOLD), ("Реализована", STATUS_SOLD), ("Забронирована", STATUS_RESERVED), ("Бронь", STATUS_RESERVED), ("В продаже", STATUS_FREE), ("Свободна", STATUS_FREE), ], ) def test_positive_forms_unchanged(badge: str, ожидаем: str) -> None: """Контроль от переусердствования: обычные формы распознаются как раньше. Ловит «починку» через грубое гашение всего, что рядом со словом «не». """ result = parse_catalog_flat(_html_with_badge(badge)) assert result.get("status") == ожидаем, f"«{badge}» перестало распознаваться" def test_negation_does_not_swallow_a_real_status_in_same_block() -> None: """Контроль: отрицание гасит СВОЙ токен, а не весь блок. «Квартира не продана. Статус: в продаже» — настоящий статус в блоке есть. Разбор по первому совпадению вернул бы None и потерял бы его. """ result = parse_catalog_flat(_html_with_badge("Квартира не продана. Статус: в продаже")) assert result.get("status") == STATUS_FREE, ( f"настоящий статус в блоке потерян: {result.get('status')!r}" ) def test_ne_inside_another_word_is_not_a_negation() -> None: """Контроль границы слова: «не» внутри другого слова не считается отрицанием.""" result = parse_catalog_flat(_html_with_badge("Цене снижена, квартира продана")) assert result.get("status") == STATUS_SOLD, ( f"«не» в хвосте слова «Цене» съело настоящий статус: {result.get('status')!r}" )