gendesign/tradein-mvp/backend/tests/test_yandex_city_sweep.py
bot-backend 79a77200d7
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 9s
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 1m43s
refactor(tradein/scrapers): удалить legacy scrape_pipeline.py + тест-хирургия (#2397 Part E1)
app.services.scrape_pipeline (3734 строк) имел 0 runtime-импортёров — Part C убрал
последнего вызывающего (admin.py, scheduler_main.py уже на
scraper_kit.orchestration.pipeline). Удаление:

- backend/app/services/scrape_pipeline.py — удалён целиком

Тесты (18 файлов, импортировавших scrape_pipeline):

DELETE (чистая legacy-оркестрация, kit-эквивалент покрыт в
test_scraper_kit_pipeline_parity{,2}.py):
- test_scrape_pipeline.py, test_scrape_pipeline_proxy_pool.py (proxy-pool DB-lease
  вытеснен RealProxyProvider DI-адаптером, покрытым test_scraper_adapters_contracts.py)
- test_anchor_watchdog.py, test_ban_rotation_1790.py, test_provider_rotation_1848.py
- test_scraper_resilience_1949_1950.py, test_sweep_drain.py

CONVERT (parity-сравнение убрано, kit-only regression оставлен/усилен):
- test_scraper_kit_pipeline_parity.py, test_scraper_kit_pipeline_parity2.py —
  _drive_old убран, _drive_new стал единственной веткой; ассёрты на конкретные
  значения counters сохранены (это теперь основное regression-покрытие kit
  orchestration)
- test_sweep_imv_phase.py — 6 IMV-phase тестов (enrich_imv true/false/no-touched/
  crash/failed-counter/cancel) переведены на run_avito_city_sweep (kit) с DI
  (config/matcher/enrichment); enrichment.process_houses_imv_batch — инжектируемый
  Protocol-метод вместо прямого импорта app.services.house_imv_backfill

PARTIAL-EDIT (легаси sweep-behavior вырезан, admin/kit-DI/несвязанные тесты
оставлены):
- test_city_sweep.py — только retarget 4 импортов на scraper_kit.orchestration.pipeline
- test_cian_city_sweep.py, test_yandex_city_sweep.py — sweep-behavior секции удалены,
  admin endpoint тесты (уже kit-DI) + anchor-timeout/watchdog формулы (retarget import)
  оставлены
- test_domclick_sweep.py — sweep-wiring тесты удалены, Counters + _map_item оставлены
- test_avito_newbuilding_sweep.py — sweep-wiring тест #5 удалён, retarget
  NewbuildingSweepCounters
- test_pipeline_browser_routing.py — 3 run_avito_pipeline теста удалены, browser-routing
  на уровне fetch_detail/fetch_house_catalog/AvitoScraper не тронут
- test_scraper_proxy.py, tests/scrapers/test_avito_anti_bot.py — _avito_proxies()
  переведён на kit (DI-параметр вместо settings-patch); pipeline-level тесты удалены

Stale-комментарии (scrape_pipeline.py как текущая реальность) поправлены в
admin.py, avito_detail_backfill.py, house_imv_backfill.py, yandex_price_history.py,
scrapers/avito_detail.py + kit-пакете (orchestration/__init__.py, orchestration
/pipeline.py, providers/avito/detail.py — strangler завершён, легаси удалён).

grep -rn scrape_pipeline app/ scripts/ packages/ — только past-tense упоминания
("удалён в Part E1", "перед его удалением") + один historical footnote
(contracts.py:138, provenance-заметка "собраны grep'ом по ... .py").

Backend suite: 3088 passed, 6 skipped (известный flake test_search_cache_hit #2208 —
не регрессия). ruff check чист на всех изменённых файлах.
2026-07-04 13:56:06 +03:00

223 lines
8.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Offline unit tests for YandexCitySweepCounters + admin sweep endpoints (#861).
Pure & fast: NO live network, NO DB.
Legacy `run_yandex_city_sweep` orchestration behavior (SERP→save per-combo,
address-enrich phase, cooperative cancel, consecutive-SERP-failure abort, bounded
retry / combo-skip logging) удалён вместе с `app.services.scrape_pipeline` (#2397
Part E1) — эквивалентная regression-coverage теперь в
`test_scraper_kit_pipeline_parity2.py::test_yandex_city_sweep` (kit
`run_yandex_city_sweep`).
Coverage:
- YandexCitySweepCounters корректно заполняются (dataclass shape)
- Combos watchdog timeout существенно больше ANCHOR_TIMEOUT_SEC (arithmetic-only)
- Admin endpoints (start/cancel/list-runs) — kit sweep вызван с DI
(config/matcher/enrichment)
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from datetime import UTC
from unittest.mock import AsyncMock
import pytest
# ── YandexCitySweepCounters ─────────────────────────────────────────────────
def test_counters_defaults() -> None:
from scraper_kit.orchestration.pipeline import YandexCitySweepCounters
c = YandexCitySweepCounters()
assert c.anchors_total == 0
assert c.lots_fetched == 0
assert c.combos_skipped == 0
assert c.address_attempted == 0
assert c.address_enriched == 0
assert c.address_failed == 0
assert c.errors_count == 0
def test_counters_to_dict_all_keys() -> None:
from dataclasses import fields
from scraper_kit.orchestration.pipeline import YandexCitySweepCounters
c = YandexCitySweepCounters()
d = c.to_dict()
expected = {f.name for f in fields(c)}
assert set(d.keys()) == expected
# ── Admin endpoint tests (offline) ──────────────────────────────────────────
@pytest.fixture
def app_with_admin():
from unittest.mock import MagicMock
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.v1 import admin as admin_module
from app.core.db import get_db
app = FastAPI()
app.include_router(admin_module.router, prefix="/api/v1/admin")
def fake_db():
yield MagicMock()
app.dependency_overrides[get_db] = fake_db
return TestClient(app)
def test_yandex_start_endpoint_validates_pages_per_anchor(app_with_admin) -> None:
"""pages_per_anchor=99 превышает max=10 → 422."""
r = app_with_admin.post(
"/api/v1/admin/scrape/yandex-city-sweep",
json={"pages_per_anchor": 99},
)
assert r.status_code == 422
def test_yandex_start_endpoint_validates_delay_too_low(app_with_admin) -> None:
"""request_delay_sec=1.0 меньше min=3.0 → 422."""
r = app_with_admin.post(
"/api/v1/admin/scrape/yandex-city-sweep",
json={"request_delay_sec": 1.0},
)
assert r.status_code == 422
def test_yandex_start_endpoint_ok(app_with_admin) -> None:
"""Valid request → 200, run_id в ответе; kit sweep вызван с DI (config/matcher/enrichment)."""
from unittest.mock import patch
from app.services.scraper_adapters import (
RealEnrichmentJobs,
RealMatcherAdapter,
RealScraperConfig,
)
with (
patch("app.services.scrape_runs.create_run", return_value=55),
patch("app.api.v1.admin.run_yandex_city_sweep", new_callable=AsyncMock) as mock_sweep,
):
r = app_with_admin.post(
"/api/v1/admin/scrape/yandex-city-sweep",
json={"pages_per_anchor": 2, "enrich_address": True},
)
assert r.status_code == 200
body = r.json()
assert body["run_id"] == 55
assert body["status"] == "running"
assert body["pages_per_anchor"] == 2
assert body["detail_top_n"] == 0
mock_sweep.assert_called_once()
kw = mock_sweep.call_args.kwargs
assert isinstance(kw["config"], RealScraperConfig)
assert isinstance(kw["matcher"], RealMatcherAdapter)
assert isinstance(kw["enrichment"], RealEnrichmentJobs)
assert "proxy_provider" in kw
def test_yandex_cancel_endpoint(app_with_admin) -> None:
"""Cancel endpoint возвращает run_id + cancelled=True."""
from unittest.mock import patch
with patch("app.services.scrape_runs.mark_cancelled", return_value=True):
r = app_with_admin.post("/api/v1/admin/scrape/yandex-city-sweep/99/cancel")
assert r.status_code == 200
body = r.json()
assert body["run_id"] == 99
assert body["cancelled"] is True
assert body["ok"] is True
def test_yandex_cancel_endpoint_not_running(app_with_admin) -> None:
"""Cancel на не-running sweep → cancelled=False."""
from unittest.mock import patch
with patch("app.services.scrape_runs.mark_cancelled", return_value=False):
r = app_with_admin.post("/api/v1/admin/scrape/yandex-city-sweep/100/cancel")
assert r.status_code == 200
assert r.json()["cancelled"] is False
def test_yandex_list_runs_endpoint(app_with_admin) -> None:
"""List runs endpoint возвращает список."""
from datetime import datetime
from unittest.mock import patch
fake_rows = [
{
"run_id": 20,
"source": "yandex_city_sweep",
"status": "done",
"params": {"pages_per_anchor": 2, "enrich_address": True},
"counters": {"lots_fetched": 200, "address_enriched": 80},
"error": None,
"started_at": datetime(2025, 5, 1, tzinfo=UTC),
"finished_at": datetime(2025, 5, 1, 1, tzinfo=UTC),
"heartbeat_at": datetime(2025, 5, 1, 1, tzinfo=UTC),
}
]
with patch("app.services.scrape_runs.list_recent", return_value=fake_rows):
r = app_with_admin.get("/api/v1/admin/scrape/yandex-city-sweep/runs")
assert r.status_code == 200
body = r.json()
assert len(body) == 1
assert body[0]["run_id"] == 20
assert body[0]["source"] == "yandex_city_sweep"
assert "2025-05-01" in body[0]["started_at"]
# ── Watchdog timeout: combos-mode budget dwarfs ANCHOR_TIMEOUT_SEC ──────────
def test_combos_sweep_timeout_substantially_larger_than_anchor_timeout() -> None:
"""В combos/center mode (anchors=None) _sweep_timeout намного > ANCHOR_TIMEOUT_SEC.
Проверяет что вычисленный таймаут для дефолтных 30 combos × 3 pages > 1500s,
т.е. не равен ANCHOR_TIMEOUT_SEC=240 и обеспечивает полный прогон sweep'а.
Не нужны мокапы — только публичные константы и арифметика из kit-модуля.
"""
from scraper_kit.orchestration.pipeline import (
_YANDEX_ADDRESS_ENRICH_BUDGET_S,
_YANDEX_COMBOS_PER_FETCH_S,
ANCHOR_TIMEOUT_SEC,
)
from app.services.scrapers.yandex_realty import DEFAULT_PRICE_RANGES, ROOM_PATH
# Параметры prod-режима
rooms_list = list(ROOM_PATH.keys()) # 5 комнатностей
price_ranges = DEFAULT_PRICE_RANGES # 6 ценовых диапазонов
max_pages = 3
request_delay_sec = 9.0 # продакшн default
num_combos = len(rooms_list) * len(price_ranges)
assert num_combos == 30, f"Expected 30 combos, got {num_combos}"
sweep_timeout = max(
ANCHOR_TIMEOUT_SEC,
num_combos * max_pages * (request_delay_sec + _YANDEX_COMBOS_PER_FETCH_S)
+ _YANDEX_ADDRESS_ENRICH_BUDGET_S,
)
# Должен быть существенно > 1500s (≈ 25 мин), что покрывает полный run.
assert sweep_timeout > 1500, (
f"sweep_timeout={sweep_timeout:.0f}s unexpectedly small — "
f"combos-mode will still timeout mid-sweep"
)
# И намного > ANCHOR_TIMEOUT_SEC (240s)
assert (
sweep_timeout > ANCHOR_TIMEOUT_SEC * 4
), f"sweep_timeout={sweep_timeout:.0f}s should be >> ANCHOR_TIMEOUT_SEC={ANCHOR_TIMEOUT_SEC}s"