gendesign/tradein-mvp/backend/tests/test_avito_offers_per_page.py
bot-backend 6ffcd78d35
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 4m27s
fix(tradein/avito): страница SERP отдаёт 60 карточек, а не 50
Замер живьём 2026-08-21 через tradein-browser (camoufox, JS исполняется):
59-60 уникальных `data-item-id` на странице выдачи. «Лишние» сверх 50 —
обычные объявления с платным продвижением (`vas-icon_type-promoted`), они
лежат в том же списке под `page-title/count`, а не отдельным рекламным
блоком, и собираются наравне с остальными.

Направление эффекта важно понимать правильно. Константа участвует ТОЛЬКО
в `ceil(total / PAGE)`, поэтому занижение размера страницы ЗАВЫШАЛО
расчётное число страниц, а не занижало:

  - запрашивали примерно на 17 % страниц больше, чем нужно; при доле банов
    37-83 % по avito-заданиям лишние запросы — основная цена ошибки;
  - `tail_loss` считался как `total - cap * 50` и завышал потерю;
  - флаг `complete` в пагинации листа чаще ложно показывал «неполно».

Тихой потери данных НЕ было: условия «страница вернула меньше PAGE
карточек, значит последняя» в коде нет, пагинация ограничена только
`max_pages`. Тест закрепляет и значение, и направление арифметики, чтобы
неверная трактовка не вернулась при следующем рефакторинге.

Найдено при разборе Авито сверкой живого браузера со скраппером; полный
разбор — в волте `research/Avito_Live_Browser_Recon_0821.md`.

Refs #3033
2026-08-21 17:38:55 +03:00

61 lines
3 KiB
Python
Raw Permalink 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.

"""Регрессия на размер страницы Avito SERP.
Замер живьём 2026-08-21 (tradein-browser / camoufox, JS исполняется): страница
выдачи отдаёт 59-60 уникальных `data-item-id`. Константа стояла 50 — то есть
`ceil(total / PAGE)` завышал число страниц примерно на 17 %, и каждый лишний
запрос — лишний шанс словить SERP firewall.
Тест намеренно проверяет НЕ только значение, но и то, что константа
по-прежнему участвует в расчёте страниц: иначе правку значения легко потерять
при рефакторинге, оставив мёртвую константу.
"""
from __future__ import annotations
import ast
import math
from pathlib import Path
from scraper_kit.providers.avito import serp
_SERP_SOURCE = Path(serp.__file__)
def test_offers_per_page_matches_live_measurement() -> None:
"""60, а не 50 — замер 2026-08-21 через реальный браузер."""
assert serp._AVITO_OFFERS_PER_PAGE == 60
def test_pages_needed_arithmetic_uses_the_constant() -> None:
"""`ceil(total / PAGE)` при 60 даёт меньше страниц, чем при 50.
Закрепляет направление эффекта: занижение размера страницы ЗАВЫШАЛО число
запрашиваемых страниц. В PR-разборе это было сформулировано наоборот, и
тест существует, чтобы неверная трактовка не вернулась.
"""
total = 1000
assert math.ceil(total / 60) == 17
assert math.ceil(total / 50) == 20
assert math.ceil(total / serp._AVITO_OFFERS_PER_PAGE) == 17
def test_constant_is_still_wired_into_pagination() -> None:
"""Константа реально используется, а не осталась мёртвой после правки."""
tree = ast.parse(_SERP_SOURCE.read_text(encoding="utf-8"))
uses = sum(
1
for node in ast.walk(tree)
if isinstance(node, ast.Name) and node.id == "_AVITO_OFFERS_PER_PAGE"
)
# 1 присваивание + минимум два места расчёта (pages_needed в бисекции и в
# пагинации листа); если станет меньше — кто-то отвязал константу.
assert uses >= 3, f"_AVITO_OFFERS_PER_PAGE упоминается {uses} раз, ожидалось >= 3"
def test_no_stale_fifty_in_page_size_comment() -> None:
"""Комментарий рядом с константой не должен утверждать «~50 карточек»."""
src = _SERP_SOURCE.read_text(encoding="utf-8")
marker = "_AVITO_OFFERS_PER_PAGE = "
idx = src.index(marker)
preceding = src[max(0, idx - 1200) : idx]
assert "~50 карточек" not in preceding