Витрина лендинга: пустой пересчёт больше не считается успешным, а итог игры «Проверьте себя» говорит, что квартиры отобраны #3550

Merged
bot-backend merged 2 commits from fix/landing-showcase-and-game into main 2026-09-17 09:21:09 +00:00
2 changed files with 99 additions and 1 deletions
Showing only changes of commit 71d60ff13e - Show all commits

View file

@ -357,7 +357,23 @@ async def _job_landing_showcase_deals(
counters = await loop.run_in_executor(
None, lambda: refresh_landing_showcase_deals(db, **kwargs)
)
ctx.runs.mark_done(db, run_id, counters)
# #3511: пустая витрина — не успех. Результатного ключа кита
# (`_RESULT_COUNTER_KEYS`) у счётчиков витрины нет, сводка просроченных
# судит её по статусу, и `done` с written=0 гасил часы свежести так же,
# как удачный прогон, — а страница тем временем теряла таблицу целиком.
if not counters.get("written"):
logger.error(
"landing_showcase_deals run_id=%d: витрина пуста (written=0, "
"considered=%s, eligible=%s) — прогон неуспешен",
run_id,
counters.get("considered"),
counters.get("eligible"),
)
ctx.runs.mark_failed(
db, run_id, "витрина пуста: прогон не записал ни одной строки (written=0)", counters
)
else:
ctx.runs.mark_done(db, run_id, counters)
except Exception:
logger.exception("scheduler: landing_showcase_deals crashed run_id=%d", run_id)
ctx.runs.mark_failed(db, run_id, "landing_showcase_deals failed", {})

View file

@ -53,6 +53,7 @@ from datetime import UTC, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
@ -211,6 +212,87 @@ def test_showcase_counters_do_not_fake_freshness() -> None:
assert sched.run_brought_data("failed", {"considered": 200, "written": 0}) is False
# ── 3а. Пустой прогон витрины не гасит часы свежести (#3511) ────────────────
class _RecordedRuns:
"""`ctx.runs`, который кладёт финал прогона строкой `_STALE_SOURCES_SQL`.
Тогда тест идёт ТЕМ ЖЕ путём, что и сводка на проде: обработчик ставит статус,
`freshness_rows` судит, принёс ли прогон данные, `stale_sources` просрочку.
Проверка «вызван mark_failed» такой путь не проходит: она зелёная и тогда,
когда статус верный, а мера свежести его не видит.
"""
def __init__(self) -> None:
self.rows: list[Any] = []
self.finished_at = NOW
def _row(self, status: str, counters: dict[str, Any]) -> None:
self.rows.append(
SimpleNamespace(
source=SOURCE,
interval_days=str(_seeded_interval_days()),
created_at=NOW - timedelta(days=30),
finished_at=self.finished_at,
status=status,
counters=counters,
)
)
def mark_done(self, db: Any, run_id: int, counters: dict[str, Any]) -> None:
self._row("done", counters)
def mark_failed(self, db: Any, run_id: int, error: str, counters: dict[str, Any]) -> None:
self._row("failed", counters)
async def _run_handler(runs: _RecordedRuns, counters: dict[str, int], age_days: float) -> None:
runs.finished_at = NOW - timedelta(days=age_days)
with patch(
"app.tasks.landing_showcase_deals.refresh_landing_showcase_deals",
return_value=counters,
):
await _job_landing_showcase_deals(MagicMock(), 1, {}, SimpleNamespace(runs=runs))
_FULL = {"considered": 200, "priced": 180, "eligible": 160, "written": 20}
_EMPTY = {"considered": 200, "priced": 180, "eligible": 160, "written": 0}
@pytest.mark.asyncio
async def test_empty_showcase_run_does_not_reset_freshness() -> None:
"""Последний непустой прогон старше 3× такта, вчерашний пустой → витрина в тревоге.
До #3511 пустой прогон завершался `done`, и `run_brought_data` (судит по
статусу: результатного ключа кита у витрины нет) засчитывал его свежестью
опустевший блок лэндинга молчал бы сколько угодно.
"""
runs = _RecordedRuns()
await _run_handler(runs, _FULL, age_days=_ACCEPTANCE_CYCLES * _seeded_interval_days() + 1)
await _run_handler(runs, _EMPTY, age_days=1)
stale = _stale_now(runs.rows)
assert [s.source for s in stale] == [SOURCE], (
f"пустой прогон витрины засчитан свежестью: статусы {[r.status for r in runs.rows]}"
)
assert stale[0].age_days > _ACCEPTANCE_CYCLES * _seeded_interval_days()
@pytest.mark.asyncio
async def test_nonempty_showcase_run_keeps_freshness() -> None:
"""Контроль: тот же сценарий с непустым последним прогоном — тревоги нет.
Зелёный с обеих сторон правки; без него правка могла бы валить любой прогон.
"""
runs = _RecordedRuns()
await _run_handler(runs, _FULL, age_days=_ACCEPTANCE_CYCLES * _seeded_interval_days() + 1)
await _run_handler(runs, _FULL, age_days=1)
assert _stale_now(runs.rows) == []
assert [r.status for r in runs.rows] == ["done", "done"]
# ── 4. Живая БД: строка реально ложится в таблицу ────────────────────────────