"""Браузерный бэкфилл Авито обязан ходить через пул прокси, а не мимо него. ДЕФЕКТ. В `avito_detail_backfill` ветка `browser_mode` конструировала `BrowserFetcher(source=..., endpoint=...)` без `proxy_provider`/`use_pool`/ `environment`. Без них фетчер не кладёт `"proxy"` в тело `POST /fetch`, сайдкар берёт свой env-прокси, и прогон уходит мимо пула целиком: ни выбора узла по affinity, ни учёта `scrape_proxy_source_bans`, ни ротации при блоке. В логе это видно как `proxy_lease_id=None`. ЭТО УЖЕ ЧИНИЛИ РЯДОМ. `house_imv_backfill` страдал ровно тем же (#2698): 35 отказов из 35 попыток в каждом прогоне полтора месяца, пока соседние свипы через ТОТ ЖЕ сайдкар тянули сотни объявлений. Контракт там закреплён тестом `test_2698_imv_sidecar_wiring.py`; здесь тот же контракт для второго пути. ЗАМЕР 27.08, прогон 5098: mode=browser, proxy_lease_id=None BLOCKED #1..#5 подряд — firewall/soft-block (browser-mode) ABORT — 5 consecutive blocks, enriched=0 attempted=5 При этом пул был здоров (4 узла, все ok, ни один не занят), а тот же URL Авито через прокси отдавал 200 и 3.3 МБ страницы — то есть площадка нас пускала, просто запрос шёл не оттуда. Проверяется исходник ровно того места, а не поведение целиком: прогнать задачу end-to-end означало бы поднять снапшоты, БД и сайдкар — цена, несоразмерная одному инварианту проводки. """ from __future__ import annotations import ast import inspect import os os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test") from app.tasks import avito_detail_backfill as adb REQUIRED = ("proxy_provider", "use_pool", "environment") def _browser_fetcher_calls() -> list[ast.Call]: """Все конструкторы BrowserFetcher в модуле.""" tree = ast.parse(inspect.getsource(adb)) return [ node for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "BrowserFetcher" ] def test_module_still_builds_a_browser_fetcher() -> None: """Страховка от проверки пустоты: если конструктор уедет, тест ниже онемеет.""" assert _browser_fetcher_calls(), ( "в avito_detail_backfill не нашлось ни одного BrowserFetcher(...) — " "тест проводки перестал что-либо проверять" ) def test_browser_fetcher_is_wired_to_the_proxy_pool() -> None: """Каждый конструктор обязан получать все три аргумента проводки пула.""" for call in _browser_fetcher_calls(): passed = {kw.arg for kw in call.keywords if kw.arg} missing = [name for name in REQUIRED if name not in passed] assert not missing, ( f"BrowserFetcher построен без {missing} — запрос уйдёт мимо пула " f"(proxy_lease_id=None), как в прогоне 5098" ) def test_pool_flag_comes_from_config_not_hardcoded() -> None: """`use_pool` читается из конфига, а не зашит константой. Зашитый `True` отнял бы у владельца выключатель `USE_PROXY_POOL_BROWSER`, а зашитый `False` вернул бы дефект, ничего не сломав заметно. """ for call in _browser_fetcher_calls(): for kw in call.keywords: if kw.arg == "use_pool": assert not isinstance(kw.value, ast.Constant), ( "use_pool задан константой — выключатель конфига перестал работать" )