"""Единственный источник правды для curl_cffi impersonate-профиля (#3034). До этого фикса `impersonate="chrome120"` был захардкожен литералом в 9+ местах scraper_kit (`providers/avito/{serp,detail,imv,houses}.py`, `orchestration/pipeline.py` x4, `providers/cian/valuation.py`, `providers/yandex/valuation.py`) — обновить TLS-профиль значило найти и поправить их все вручную, и один пропущенный литерал молча гонял бы устаревший профиль. Теперь единственный источник — `DEFAULT_IMPERSONATE` (`providers/_base.py`); любой caller обязан читать константу. ПОЧЕМУ ГРЕП, А НЕ AST. Инвариант — «строка `chrome120` не встречается нигде в дереве» (ни в коде, ни в docstring/комментарии) — буквально то, что нужно поймать: сменится профиль — не должно остаться ни одного места, которое надо будет вспомнить и найти руками. AST увидел бы только строковые константы в выражениях (kwarg-значения, docstring), но не поймал бы упоминание в `#`-комментарии — а именно в комментарии литерал незаметнее всего переживёт следующий рефактор. Область — ДВЕ директории: * `packages/scraper-kit/src/scraper_kit/` — пакет, где живёт `DEFAULT_IMPERSONATE` и все providers; * `backend/app/` — прод-код бэкенда. Добавлен после того, как выяснилось, что первая волна #3034 вычистила kit, а три живых прод-пути остались на `chrome120`: верификация куки Циана (`services/cian_session.py`) и два яндексовых бэкфилла. Оговорка «legacy-периметр вне scope» разошлась с фактом: периметр не спит, он ходит в сеть каждый день, и та же ссылка (#2361 F4a) к тому моменту была закрыта, то есть отсылать было уже некуда. НЕ входят: `tests/fixtures/**` (там номер профиля — часть записи о том, чем снят фикстур-HTML, историю переписывать нельзя) и `scripts/**` (разовые инструменты, в прод-путях не участвуют). """ from __future__ import annotations from pathlib import Path _BACKEND_ROOT = Path(__file__).resolve().parents[1] _KIT_SRC = _BACKEND_ROOT.parent / "packages" / "scraper-kit" / "src" / "scraper_kit" _APP_SRC = _BACKEND_ROOT / "app" _SCAN_ROOTS = (_KIT_SRC, _APP_SRC) _BANNED_LITERAL = "chrome120" def test_scan_area_exists() -> None: """Обе области сканирования живы — иначе сторож зелен вхолостую (путь съехал).""" for root in _SCAN_ROOTS: assert root.is_dir(), f"область сканирования съехала: {root}" def test_detector_actually_detects(tmp_path: Path) -> None: """Сторож обязан уметь краснеть на литерале — иначе он зелен вхолостую.""" probe = tmp_path / "probe.py" probe.write_text('impersonate = "chrome120"\n', encoding="utf-8") assert _BANNED_LITERAL in probe.read_text(encoding="utf-8") clean = tmp_path / "clean.py" clean.write_text( "from scraper_kit.providers._base import DEFAULT_IMPERSONATE\n", encoding="utf-8" ) assert _BANNED_LITERAL not in clean.read_text(encoding="utf-8") def test_default_impersonate_is_the_only_chrome_profile_literal() -> None: """Ни один файл scraper_kit не содержит строку "chrome120" — ни в коде, ни в docstring/комментарии. Единственное разрешённое место для конкретного номера профиля — значение `DEFAULT_IMPERSONATE` в `providers/_base.py`. """ offenders = [ str(path.relative_to(_BACKEND_ROOT.parent)) for root in _SCAN_ROOTS for path in sorted(root.rglob("*.py")) if _BANNED_LITERAL in path.read_text(encoding="utf-8") ] assert offenders == [], ( f"{offenders}: литерал {_BANNED_LITERAL!r} обходит DEFAULT_IMPERSONATE " "(providers/_base.py) — единственный источник правды для impersonate-" "профиля. Прочитай константу вместо хардкода строки." )