gendesign/tradein-mvp/browser/test_server_recycle.py
bot-backend b9025de666
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
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 / browser-tests (pull_request) Successful in 1m5s
fix(tradein/domclick): площадка отдаёт одну карточку на процесс браузера, а мы держали один на весь прогон (#3205)
Замер на проде 2026-08-29. Три независимых запуска camoufox через прод-прокси,
идём по карточкам до первого отказа — каждый раз одно и то же: карточка #1
даёт 200 и ~1 МБ с SSR-стейтом, карточка #2 даёт 401 и страницу отказа на
26 625 байт. A/B на восьми карточках: свой браузер на каждую — 4 из 4 успеха,
один браузер на четыре — 1 из 4. Прямой выход с сервера и выход через прокси
неразличимы (1 из 8 в обоих условиях), то есть дело не в IP.

Свежего КОНТЕКСТА не хватает: в контрольном замере каждая карточка бралась
через browser.new_page(), в новом изолированном контексте, — и всё равно отказ
со второй. Признак живёт на уровне процесса, camoufox генерирует отпечаток при
запуске, а не при создании контекста. Отсюда: reset_context (#3118) эту задачу
не решает в принципе. Цена перезапуска — 0.6 с (3.5 с только первый, холодный).

- PROVIDERS: добавлен "domclick" (+ host-detect). Раньше он проваливался в
  generic и делил браузер со счётчиком страниц с прочим трафиком — при пороге
  перезапуска 1 это было бы неверно.
- BROWSER_RECYCLE_PAGES стал поставщик-зависимым (_resolve_recycle_pages +
  BROWSER_RECYCLE_PAGES_{PROVIDER}), по образцу BROWSER_BLOCK_IMAGES_{PROVIDER}
  из #3185. Код-дефолт domclick=1, остальным прежние 15 — у Авито и Циана узор
  другой и своего замера под него нет.

Отдельно починены ~24 холостых охранника в тестах. Они делали
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000), чтобы запретить
перезапуск браузера; после перехода на словарь этот патч перестал на что-либо
влиять, и набор оставался зелёным лишь потому, что ни один тест не делает 15
страниц подряд. Теперь патчится _RECYCLE_PAGES_BY_PROVIDER, а сама глобальная
константа убрана, чтобы её не патчили снова. Проверено мутацией: при пороге 1
для всех провайдеров падают ровно три теста, которые этот дефолт и проверяют,
остальные 155 удерживаются — значит охранники работают.
2026-08-29 13:14:20 +03:00

74 lines
3.7 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.

"""test_server_recycle.py — юниты для per-provider recycle_pages threshold (#3205).
Проверяет ``_resolve_recycle_pages``:
1. per-provider env перебивает глобальный env;
2. глобальный env перебивает код-дефолт per-provider;
3. невалидное (не int) значение → фолбэк, без исключения;
4. значение <1 → приводится к 1;
5. domclick без env → код-дефолт 1 (живой замер #3205: одна карточка на процесс);
6. avito/cian/yandex без env → общий фолбэк 15 (их узор бана не менялся).
camoufox/Playwright НЕ поднимается: тестируется чистая функция резолвера.
Запуск (из tradein-mvp/browser/)::
python -m pytest test_server_recycle.py -q
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
# server.py — не пакет (отдельный сервис без __init__/pyproject). Грузим по пути.
_SERVER_PATH = Path(__file__).resolve().parent / "server.py"
_spec = importlib.util.spec_from_file_location("tradein_browser_server", _SERVER_PATH)
assert _spec is not None and _spec.loader is not None
server = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(server)
def test_per_provider_env_overrides_global() -> None:
"""BROWSER_RECYCLE_PAGES_DOMCLICK=3 + глобал=10 → domclick=3."""
fake_env = {"BROWSER_RECYCLE_PAGES": "10", "BROWSER_RECYCLE_PAGES_DOMCLICK": "3"}
assert server._resolve_recycle_pages("domclick", fake_env) == 3
def test_global_env_overrides_code_default() -> None:
"""Нет per-provider env, но задан глобальный → перебивает код-дефолт domclick=1."""
fake_env = {"BROWSER_RECYCLE_PAGES": "7"}
assert server._resolve_recycle_pages("domclick", fake_env) == 7
def test_invalid_per_provider_env_falls_back() -> None:
"""Невалидный (не int) per-provider env → фолбэк на глобальный уровень."""
fake_env = {"BROWSER_RECYCLE_PAGES": "9", "BROWSER_RECYCLE_PAGES_CIAN": "abc"}
assert server._resolve_recycle_pages("cian", fake_env) == 9
def test_invalid_global_env_falls_back_to_code_default() -> None:
"""Невалидный глобальный env → фолбэк на код-дефолт per-provider (без исключения)."""
fake_env = {"BROWSER_RECYCLE_PAGES": "not-a-number"}
assert server._resolve_recycle_pages("domclick", fake_env) == 1
assert server._resolve_recycle_pages("cian", fake_env) == 15
def test_value_below_one_clamped_to_one() -> None:
"""Явный env <1 недопустим → приводится к 1, а не проваливается дальше по цепочке."""
assert server._resolve_recycle_pages("avito", {"BROWSER_RECYCLE_PAGES_AVITO": "0"}) == 1
assert server._resolve_recycle_pages("cian", {"BROWSER_RECYCLE_PAGES": "-5"}) == 1
def test_domclick_without_env_defaults_to_one() -> None:
"""#3205: без единого env — domclick=1 (одна карточка на процесс camoufox)."""
assert server._resolve_recycle_pages("domclick", {}) == 1
def test_other_providers_without_env_default_to_fifteen() -> None:
"""avito/cian/yandex без env — общий фолбэк 15, узор бана у них не менялся."""
for provider in ("avito", "cian", "yandex"):
assert server._resolve_recycle_pages(provider, {}) == 15
def test_generic_without_env_defaults_to_fifteen() -> None:
assert server._resolve_recycle_pages("generic", {}) == 15