gendesign/tradein-mvp/backend/tests/test_backfill_honest_status.py
bot-backend e9ca744e85
All checks were successful
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / 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 5m4s
fix(tradein/scrapers): не путать rows_inserted/processed с честным результатным ключом
Ревью честного run-status нашло, что _RESULT_COUNTER_KEYS ловил не только целевой
yandex_newbuilding_sweep, но и rosreestr_dkp_import (rows_inserted, 66 из 67 прод-
прогонов = здоровый ноль догнавшего инкрементального импорта) и newbuilding_enrich
(processed — счётчик попыток, ==limit даже при частичном провале). Первое завело бы
практически непрерываемый ложный zero-стрик у здорового источника, второе маскировало
бы реальные отказы под measured-N.

Проверено по прод-БД (2026-08-15): "succeeded" пишут ТОЛЬКО yandex_newbuilding_sweep
(42 прогона/90д) и newbuilding_enrich (65/90д) — ни разу rosreestr_dkp_import; у
yandex_newbuilding_sweep succeeded численно совпадает с rows_inserted на всех 42/42
прогонах. Заменил "rows_inserted"+"processed" на "succeeded" в _RESULT_COUNTER_KEYS
(app-копия и byte-эквивалентная kit-копия) — цель (b) исходной правки сохранена, ложный
стрик у rosreestr_dkp_import снят, попутно newbuilding_enrich получает честное
измерение вместо счётчика попыток.

Также поправлены докстринги test_backfill_honest_status.py — два кейса (76%/72%
отказов -> 'done') проверяют только выбор финализатора mark_backfill_finished
(mark_done там замокан); реальный mark_done с honest-run-status переквалифицирует их
в 'failed' через _failed_ratio_too_high — это не документировалось явно.
2026-08-15 18:49:37 +03:00

100 lines
6.4 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.

"""#2674 — detail-backfill с нулём обогащений перестаёт называться 'done'.
Все три backfill'а (avito/yandex/domclick) финализировались одним mark_done.
На проде 2026-08-06 это 78 прогонов из 158: avito 23/76 (включая 5 прогонов на
1500-1600 попыток без единого обогащения), yandex 31/52, domclick 24/30
(494 попытки → 0 обогащено, 63 блока, 431 fail — и все 30 'done').
Проверяем ровно ветвление mark_backfill_finished — БД замокана (mark_done/mark_failed/
mark_banned здесь fake-заглушки, регистрирующие ТОЛЬКО факт вызова). Это значит: кейсы
ниже с высокой долей отказов (attempted=50, failed=38 или 36 — 76%/72%), ожидающие
'done', проверяют лишь то, КАКОЙ финализатор ВЫБРАЛ mark_backfill_finished (#2674:
"обогатили хоть что-то — успех"), а НЕ то, что реально запишет в БД mark_done. С
honest-run-status (2026-08-15) mark_done САМ переквалифицирует такой прогон в 'failed'
через _failed_ratio_too_high (доля отказов >= 0.5) — реальный терминальный статус
для этих двух кейсов на проде теперь 'failed', не 'done'. Это намеренно проверяется
отдельно, БЕЗ мока mark_done, в tests/test_honest_run_status_failed_ratio.py
(test_prod_fact_avito_15_08_no_longer_done и соседние) — не читай эти два кейса как
"76%/72% отказов = 'done' в проде".
"""
from __future__ import annotations
import os
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from app.services import scrape_runs as runs_mod
def _finish(counters: dict[str, int], *, aborted: bool = False) -> tuple[str, str | None]:
"""Вызвать mark_backfill_finished с замоканными mark_* → (статус, причина)."""
calls: list[tuple[str, str | None]] = []
with (
patch.object(runs_mod, "mark_done", lambda *a, **k: calls.append(("done", None))),
patch.object(
runs_mod, "mark_failed", lambda db, rid, err, c: calls.append(("failed", err))
),
# **k — mark_banned принимает ещё и ban_kind (#2686/#2764); этот тест про
# ВЫБОР финализатора, диагноз проверяет test_2764_ban_kind_no_default.py.
patch.object(
runs_mod, "mark_banned", lambda db, rid, err, c, **k: calls.append(("banned", err))
),
):
runs_mod.mark_backfill_finished(
MagicMock(),
1,
counters,
source="domclick_detail_backfill",
aborted_by_blocks=aborted,
)
assert len(calls) == 1, f"ожидался ровно один финализатор, получено {calls}"
return calls[0]
@pytest.mark.parametrize(
("counters", "aborted", "expected"),
[
# Прод-факт domclick: 16 прогонов attempted=3 blocked=3 → брейкер оборвал.
({"attempted": 3, "enriched": 0, "blocked": 3, "failed": 0}, True, "banned"),
# Прод-факт domclick: 100 попыток, все fail, блоки не распознаны (до #2645).
({"attempted": 100, "enriched": 0, "blocked": 0, "failed": 100}, False, "failed"),
# Прод-факт avito: 1500 попыток, 1499 fail + 1 блок, ноль обогащений.
({"attempted": 1500, "enriched": 0, "blocked": 1, "failed": 1499}, False, "banned"),
# Прод-факт yandex: 31 прогон attempted=5 failed=5, ключа blocked нет вовсе.
({"attempted": 5, "enriched": 0, "failed": 5}, False, "failed"),
# Кандидатов не было — честная пустота, это успех.
({"attempted": 0, "enriched": 0, "blocked": 0, "failed": 0}, False, "done"),
# Частичный прогон: обогатили хоть что-то → mark_backfill_finished ВЫБИРАЕТ
# mark_done как финализатор (#2674). 76% отказов (38 из 50) — здесь mark_done
# замокан, поэтому статус остаётся 'done'; в реальном mark_done с
# honest-run-status (2026-08-15) это переквалифицируется в 'failed'
# (_failed_ratio_too_high, доля >= 0.5) — см. докстринг модуля.
({"attempted": 50, "enriched": 12, "blocked": 0, "failed": 38}, False, "done"),
# Блоки были, но прогон доработал и обогатил — mark_backfill_finished выбирает
# НЕ 'banned'. 72% отказов (36 из 50) — та же оговорка: реальный mark_done
# переквалифицирует в 'failed', см. докстринг модуля выше.
({"attempted": 50, "enriched": 12, "blocked": 2, "failed": 36}, False, "done"),
# Блок оборвал прогон, хотя часть успели обогатить — работа не доделана.
({"attempted": 50, "enriched": 12, "blocked": 5, "failed": 33}, True, "banned"),
# avito: 404-«снято с продажи» — тоже результат, а не пустой прогон.
({"attempted": 30, "enriched": 0, "gone": 30, "blocked": 0, "failed": 0}, False, "done"),
],
)
def test_status_matches_reality(counters: dict[str, Any], aborted: bool, expected: str) -> None:
status, _ = _finish(counters, aborted=aborted)
assert status == expected
def test_reason_carries_numbers_and_marker() -> None:
"""Причина в scrape_runs.error должна быть читаемой человеком, не пустой."""
status, reason = _finish({"attempted": 3, "enriched": 0, "blocked": 3}, aborted=True)
assert status == "banned"
assert reason is not None
assert "backfill-honest-status" in reason
assert "domclick_detail_backfill" in reason
assert "blocked=3" in reason and "из 3 попыток" in reason