"""#2687: распознанный QRATOR-блок домкликового свипа — это 'platform', не 'unknown'. Ветка `if counters.blocked:` в `run_domclick_city_sweep` входится ТОЛЬКО когда скрейпер поднял `DomClickBlockedError` (площадка отдала QRATOR block-страницу). Это ровно определение `BAN_KIND_PLATFORM`. До правки свип звал `mark_banned` без `ban_kind`, и все его баны падали в дефолт 'unknown' (#2764) — диагноз был УСТАНОВЛЕН (блок распознан), но не передавался, и мониторинг не мог отличить «площадка отбила» (platform) от «тракт/прокси» (infra). Прод 2026-08-12: domclick_city_sweep 3751/3675/3594 все `banned` с `blocked=1`, `ban_kind='unknown'`. На старом коде тест ниже падает (ban_kind='unknown'). """ from __future__ import annotations import os from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock, patch os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db") from scraper_kit.orchestration.pipeline import run_domclick_city_sweep PFX = "scraper_kit.orchestration.pipeline" class _BanKindRecorder: """Ловит ban_kind, переданный в mark_banned (остальное — no-op).""" def __init__(self) -> None: self.ban_kind: str | None = None self.marked: list[str] = [] def is_cancelled(self, db: Any, run_id: int) -> bool: return False def update_heartbeat(self, db: Any, run_id: int, counters: dict[str, Any]) -> None: return None def mark_done(self, db: Any, run_id: int, counters: dict[str, Any]) -> None: self.marked.append("done") def mark_failed(self, db: Any, run_id: int, error: str, counters: dict[str, Any]) -> None: self.marked.append("failed") def mark_banned( self, db: Any, run_id: int, error: str, counters: dict[str, Any], **kw: Any ) -> None: self.marked.append("banned") self.ban_kind = kw.get("ban_kind") async def _drive_blocked() -> _BanKindRecorder: recorder = _BanKindRecorder() scraper = MagicMock() scraper.__aenter__ = AsyncMock(return_value=scraper) scraper.__aexit__ = AsyncMock(return_value=None) scraper.fetch_city = AsyncMock(return_value=[MagicMock() for _ in range(39)]) scraper.blocked = True # распознанный QRATOR-блок scraper.geo_filtered = 0 scraper.fetch_errors = 1 scraper.buckets_completed = 2 scraper.buckets_total = 6 with ( patch(f"{PFX}.DomClickScraper", return_value=scraper), patch(f"{PFX}.save_listings", MagicMock(return_value=(39, 0))), patch(f"{PFX}.runs", recorder), ): await run_domclick_city_sweep( MagicMock(), config=SimpleNamespace(browser_http_endpoint="http://x:9000"), matcher=MagicMock(), run_id=1, city_id=4, pages=1, request_delay_sec=0.0, ) return recorder async def test_qrator_block_marks_platform_ban_kind() -> None: """Блок распознан → ban_kind='platform'. На старом коде было 'unknown' (дефолт).""" recorder = await _drive_blocked() assert recorder.marked[-1] == "banned" assert recorder.ban_kind == "platform"