gendesign/tradein-mvp/backend/tests/test_3074_checkpoint_survives_claim.py
bot-backend e4b3c6cc2b
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-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 / backend-tests (pull_request) Successful in 4m35s
fix(tradein/scheduler): чекпоинт предшественника переживает обрыв до первой новой корзины (#3074)
Прод-факт: прогон 4707 (23.08) подхватил 42 корзины у 4117, был убит деплоем
на 26-й минуте до завершения первой НОВОЙ корзины — и не успел ни разу
написать heartbeat с done_buckets. Его собственный чекпоинт пуст: следующий
кандидат увидел бы no_checkpoint, цепочка оборвалась бы с потерей 42 корзин.

_resume_decision при вердикте 'ok' теперь кладёт done_buckets предшественника
в counters-заготовку нового прогона — она персистится при claim, до старта
пайплайна. Heartbeat мержит jsonb: первый настоящий bucket-heartbeat
перезапишет ключ надмножеством, двойной записи нет. Отказные вердикты
чекпоинт не наследуют (прогон с нуля не должен врать о собранном).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:35:44 +05:00

63 lines
3.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.

"""#3074: чекпоинт предшественника наследуется в counters НОВОГО прогона при claim.
Прод-факт (Beget, 23.08): прогон 4707 подхватил чекпоинт 4117 (42 корзины,
resume_reason='ok'), был убит деплоем на 26-й минуте — ДО завершения первой
НОВОЙ корзины — и не успел ни разу написать heartbeat с done_buckets. Его
собственный чекпоинт остался пуст: следующий кандидат увидел бы no_checkpoint,
и 42 корзины пропали бы, хотя цепочка (resume_chain=2) ещё позволяла подхват.
Фикс: _resume_decision при вердикте 'ok' кладёт done_buckets предшественника в
counters-заготовку нового прогона — она пишется в БД прямо при claim
(_pick_resume → update_heartbeat), до старта пайплайна. Heartbeat мержит jsonb,
первый настоящий bucket-heartbeat перезапишет ключ надмножеством.
Красный на origin/main по ЗНАЧЕНИЮ: 'done_buckets' отсутствует в вердикте.
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from types import SimpleNamespace
from typing import Any
from scraper_kit.orchestration import scheduler as sched
def _candidate(**over: Any) -> SimpleNamespace:
"""Кандидат из _RESUME_CANDIDATE_SQL: прогон 4117 как он лежал на проде 23.08."""
base = {
"prev_id": 4117,
"prev_status": "banned",
"prev_counters": {
"resume_chain": 1,
"done_buckets": [f"room_1_komn:{i}:0" for i in range(42)],
},
"same_params": True,
"age_h": 164.8,
"interval_days": "7",
}
base.update(over)
return SimpleNamespace(**base)
def test_ok_verdict_carries_predecessor_checkpoint() -> None:
"""Вердикт 'ok' несёт done_buckets предшественника — чекпоинт персистится при
claim и переживает обрыв до первой новой корзины (кейс 4707)."""
resume_id, verdict = sched._resume_decision(_candidate())
assert resume_id == 4117
assert verdict["resume_reason"] == "ok"
assert verdict.get("done_buckets") == sorted(f"room_1_komn:{i}:0" for i in range(42)), (
"counters-заготовка нового прогона не содержит чекпоинт предшественника — "
"обрыв до первой новой корзины снова потеряет всю цепочку"
)
def test_refusal_verdict_carries_no_checkpoint() -> None:
"""Отказ от подхвата чекпоинт не наследует: прогон честно идёт с нуля, и его
counters не должны врать, будто корзины предшественника уже собраны."""
_id, verdict = sched._resume_decision(_candidate(same_params=False))
assert verdict["resume_reason"] == "params_changed"
assert "done_buckets" not in verdict