Рефакторинг рендера [этап 0/6]: golden-эталоны фидов (оракул эквивалентности) #27
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Часть эпика «Рефакторинг пайплайна рендера». Спека:
plans/2026-07-05-render-pipeline-refactor.md(v6), раздел «Этап 0». Выполняется ПЕРВЫМ, до любых правок кода. Line-refs действительны дляf9550d8.Цель
Снять полные снапшоты выходов
generate_channel_rss(RSS XML) иgenerate_channel_html(HTML-фид) на корпусе сообщений и закрепить их тестом сравнения. Это оракул эквивалентности для всех этапов: изменение байтов фида без ссылки на пункт реестра §3 спеки = регрессия.Корпус: записанные РЕАЛЬНЫЕ сообщения с прод-сервера (v6)
Источник — рабочий кэш бриджа
data/tgcache/: пиклы ровно нужного формата —{channel}.cache={'timestamp','limit','messages': List[Message]}(tg_cache._save_history_to_cache) и{channel}.chatinfo(дляcached_get_chat).Снапшот кэша УЖЕ снят и проверен (2026-07-05):
data/tgcache_prod_2026-07-05/— 90.cache+ 89.chatinfo, 53 МБ (вне git). Совместимость пиклов подтверждена: все 179 файлов распикливаются под kurigram 2.2.23 без ошибок. Инвентарь (8582 сообщения): PHOTO 5397, text-only 1609, WEB_PAGE 764 (с фото 581 / без 183), VIDEO 653, DOCUMENT 51, ANIMATION 33, POLL 31, VIDEO_NOTE 15, STICKER 13, AUDIO 8, VOICE 6, GIVEAWAY/GIVEAWAY_WINNERS по 1; media_group-членов 3159, forward 670, reply 357, time-кластерных пар (gap≤5s) ~294; реакции: обычных 17981, custom 1018, paid 593, пустых reactions-объектов 17 (§3.15-edge покрыт реальными данными); зачёркиваний 150 (§3.1 покрыт); ВСЕ даты naive; None-date — 0.Задача этапа: отобрать из снапшота пары файлов (несколько каналов, суммарно покрывающих инвентарь: media_groups, time-кластеры, poll, webpage±фото, forward, reply, реакции всех видов, зачёркивание) → закоммитить в
tests/test_data/recorded/→ написать реплей-загрузчик и снять golden вtests/test_data/golden/.Реплей: загрузчик распикливает файлы напрямую (
timestamp/limitигнорируются — никакой проверки свежести) и monkeypatch'итtg_cache.cached_get_chat_history/cached_get_chatна возврат записанных объектов. Это буквально прод-путь cache-hit. Lazy-import внутри фид-функций разрешает имена поздно — патч модуля tg_cache работает (образец — test_stage4_eventloop.py).Синтетическая добавка — только недостижимое в записанном: None-date пост (добавляется этапом 4, не здесь). Отсутствующие в корпусе типы (PAID_MEDIA, STORY, LIVE_PHOTO, CHECKLIST и пр. экзотика) покрываются моками на fragment-уровне этапа 5, в feed-golden их нет.
НЕ входят в golden: сценарии exclude_flags/exclude_text (фильтры не меняют байты выживших постов — семантика закрепляется парой unit-тестов); None-date пост (текущий код падает: naive/aware TypeError в sort-ключе групп, rss_generator.py:141).
Обязательные меры детерминизма (все шесть)
os.environ['TZ'] = 'UTC'; time.tzset()в conftest (naive-даты интерпретируются.timestamp()в локальной TZ).<lastBuildDate>— feedgen 1.0.0 ставит now() один раз в конструктореFeedGenerator(меняется между прогонами);<generator>— версии не содержит, нормализация — опциональная страховка.list(set(...)), rss_generator.py:260–265, PYTHONHASHSEED): содержимое<div class="message-flags">нормализуется сортировкой. Снимается этапом 2 (§3.8).monkeypatch.setattr(KeyManager, "signing_key", ...)(образец — tests/test_new_media_types.py) — иначе свежий checkout на CI генерирует новый ключ и все digest'ы меняются.upsert_media_file_ids_bulk_sync(илиDB_PATH→ tmp_path;DB_PATHcwd-относителен, file_io.py:13). Образец — test_stage4_eventloop.py:164.time_based_merge=True: ключ читает толькоrss_generator.Config— его патча достаточно (post_parser.Config— независимый dict; патчить оба — дешёвая страховка).Ожидаемые обновления golden по этапам (контекст)
Этап 1 (§3 пп.1, 3, 4; пп.2 и 5 — fail-closed, golden не видит — юнит-тесты sanitizer); этап 2 (пп.6, 8, 15 + снятие нормализации флагов); этап 4 (None-date фикстура в HTML-golden); этап 5b (п.14).
DoD
pytestполностью зелёный.Общие правила эпика
Ветка
refactor/render-pipelineот f9550d8; один этап = один коммит; комментарии в коде — English; точечные правки.Отбор корпуса выполнен (жадно по матрице признаков из инвентаря), файлы уже лежат в
tests/test_data/recorded/(4 канала ×.cache+.chatinfo, 2.7 МБ, пока untracked — закоммитить первым коммитом этапа):bladerunnerblues(1.2 МБ) — единственный источник GIVEAWAY + GIVEAWAY_WINNERS; POLL, pdf, DOCUMENT, ANIMATION, r_paid×71, wp_nophoto;theyforcedme(427 КБ) — STICKER + AUDIO + VOICE + VIDEO_NOTE + POLL + ANIMATION в одном канале; forward×13, reply×10;embedoka(612 КБ) — r_empty + r_custom×35 + strike×7 + POLL + pdf + wp_nophoto; forward×20, reply×14;meow_design(536 КБ) — 51 time-кластерная пара (ядро time_based_merge), media_groups×22, pdf, POLL, strike.Суммарное покрытие: PHOTO 246, VIDEO 40, media_groups 66, forward 35, reply 27, wp_photo 11 — полная матрица закрыта,
.chatinfoесть у всех четырёх.Кодеру остаётся: реплей-загрузчик + меры детерминизма из ишью + снятие golden.