test(render): этап 0 — golden-эталоны фидов (оракул) (#27) #35
Reference in New Issue
Block a user
Delete Branch "refactor/render-stage0-golden"
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?
Summary
Этап 0/6 эпика рефакторинга рендер-пайплайна (#34, closes #27): golden-эталоны фидов как оракул эквивалентности, снятые ДО любых правок кода. Полные snapshot'ы
generate_channel_rss(RSS XML) +generate_channel_html(HTML) на записанном РЕАЛЬНОМ корпусе (4 канала) закреплены тестом сравнения. Для всех следующих этапов: изменение байтов фида без ссылки на §3 реестра спеки = регрессия. Продакшн рендер-код не тронут — только тест-инфра.Что вошло
tests/golden_replay.py— реплей-загрузчик (распикливаетrecorded/*.cache|*.chatinfo, monkeypatchtg_cache.cached_get_chat_history/cached_get_chat— буквально прод-путь cache-hit, образецtest_stage4_eventloop.py) + генератор goldens + нормализаторы.tests/test_stage0_golden.py— оракул: 11 тестов (RSS×4, HTML×4 + 3 guard'а на нормализаторы/наличие goldens).tests/test_data/golden/— 8 замороженных snapshot'ов (~1.9 МБ; item/post: bladerunnerblues 90, embedoka 84, meow_design 36 merged, theyforcedme 89).tests/conftest.py— пинTZ=UTC(naive-даты kurigram интерпретируютсяdatetime.timestamp()в ЛОКАЛЬНОЙ tz → без пина<pubDate>дрейфует; песочница — MSK).Нормализации (минимальные, симметрично golden+actual, каждая с источником недетерминизма)
<lastBuildDate>(feedgen ставитnow()в конструкторе);<generator>(feedgen 1.0.0 не эмитит — страховка от апгрейда либы);<div class="message-flags">(merged-посты строят флаги какlist(set(...)), hash-порядок; убирается коммитом этапа 2 по §3.8).Сознательно НЕ добавлял нормализаций сверх этих (реакции, например, приходят фиксированного порядка из пикла — 0 дрейфа). Детерминизм подтверждён 3 способами: двойной in-process захват, разные
PYTHONHASHSEED, регенерация в ОТДЕЛЬНОМ процессе → нормализованный вывод байт-идентичен по всем 8 файлам.How verified
python3.13 -m venv .venv+pip install -r requirements.txt(kurigram 2.2.23 и пр.) — без блокеров. Все 4 пары распикливаются под 2.2.23.test_stage0_golden.py: 11 passed. Полный сюит: 332 passed. Прогнал независимо.⚠️ Два judgment call'а — нужен твой ack (это оракул, важно зафиксировать «правильную» базу)
time_based_merge=Trueпри захвате. В параграфе про snapshot это прямо не сказано, но чеклист детерминизма спеки это провизионит («фикстура time-based кластера требуетtime_based_merge=True»), а каналmeow_designсуществует именно как time-cluster ядро — захват с ним OFF оставил бы весь этот путь непокрытым оракулом. Включил единообразно (пин иrss_generator.Config, иpost_parser.Config). Если хотел дефолт (False) — скажи, перезахвачу._reply_enrichmentделает ЖИВОЙclient.get_messages— вне cache-hit пути, что мокает спека. С голым тест-клиентом оно стабильно no-op'ит (ошибка логируется/глотается) и падает обратно на записанный shallowreply_to_messageиз пикла. Детерминировано и верно «мокаем только tg_cache»; синтетические reply-таргеты НЕ фабриковал (это инъекция в оракул против guardrail'а «заморозить текущее поведение»). RSS не затронут.Checklist
🤖 Авто-ревью · #35 (test(render): этап 0 — golden-эталоны фидов (оракул), #27), голова
dc9a23f8Вердикт: PASS ✅ (review/approved). Аккуратный equivalence-oracle: чисто аддитивный (прод-рендер не тронут), детерминированный, покрытие плотное. Мержибельно.
Проверено (реальный прогон, CI-fidelity)
<lastBuildDate>(feedgennow()) стрипается, hash-упорядоченныйlist(set(...))merged-flags div сортируется. Подписанные/media-URL детерминированы через фиксированныйGOLDEN_SIGNING_KEY(monkeypatchKeyManager.signing_key) — «golden воспроизводится на любой машине». Плюс тесты на сами нормализаторы (test_normalization_sorts_merged_flags,test_normalization_strips_lastbuilddate) — load-bearing логика залочена.golden_replay.pyраспикливаетrecorded/*.cache|.chatinfo, monkeypatchcached_get_chat_history/cached_get_chat(буквально cache-hit прод-путь, образецtest_stage4_eventloop.py);timestamp/limitигнорятся как в проде.conftest.pyставит config-mock ДО любого теста (снимает sys.modules['config'] order-pollution).pip install -r requirements.txt(CI-fidelity, kurigram 2.2.23);test_stage0_golden.py— 11/11 (4 RSS + 4 HTML golden + present-nonempty + 2 нормализатора); полныйpytest tests/— 332 passed (321 + 11 новых), ноль регрессий от нового conftest/фикстур.mock_configвместо реального конфига. Оракул замораживает ТЕКУЩЕЕ поведение (вкл. известные баги) намеренно — это и есть контракт этапа 0 (следующие этапы: изменение байтов без ссылки на §3 реестра = регрессия).Заметка (не блокер)
pickle.loadнаrecorded/*.cache— это СОБСТВЕННЫЙ закоммиченный корпус проекта (доверенные тест-фикстуры, зеркалит прод-формат кэша), не внешний ввод → приемлемо для тест-инфры. Ниже DROP-floor.test_stage4_eventloop.py).Детерминизм golden-фидов (lastBuildDate/flags/подписанные media-URL) проверил как ключевой риск — снят корректно (11/11 + 332 full suite зелёные, fixed signing key). Прод-код не тронут. Ветка чистая.
ACK: time_based_merge=True — да, оставляй, не перезахватывай. Это не просто «ок», это единственно правильная база, и вот почему:
Прод так и работает — в dockercompose TIME_BASED_MERGE: True. Оракул обязан отражать продакшн, а не голый code-default (в config.py он False, но это неважно — прод его переопределяет). Сверил сам.
Это ЕДИНСТВЕННЫЙ способ покрыть time-clustering путь, который переписывает этап 4. С False весь _create_time_based_media_groups / _compute_time_based_group_ids остался бы вне golden — и тогда deepcopy→чистая-функция эквивалентность этапа 4 (#31) верифицировалась бы против пустоты. meow_design как реальный time-cluster канал — ровно то, что нужно.
Детерминизм гарантирован именно на этом корпусе: проверил — 400 сообщений, 0 None-date. Нестабильность True была бы только от datetime.now()-фолбэков на None-date постах, а их нет. Так что True здесь чист.
Твоя интуиция верна на всех трёх осях — фиксируем True как базу оракула для всего эпика.
Про reply-enrichment (второй пункт): решение принято правильно, но это оформленный пробел, не «мелочь». Не фабрикуй синтетику в golden — корпус должен оставаться реальным, тут ты прав. Но _reply_enrichment (HTML-only, живой get_messages мимо кэша) в харнессе no-op'ит → golden эту ветку не видит. А этап 3 (#30) как раз переносит enrichment в _prepare_feed_posts. Поэтому: этап 3 обязан нести отдельный точечный unit-тест на enrichment (мок client.get_messages с заполненным .reply_to_message; проверить батчинг по chat_id + проставление + рендер reply-блока). Записал это требование прямо в #30 — иначе перенос enrichment уедет непроверенным. На этапе 0 no-op принимается как есть.
Green-light: запускай этап 1 (sanitizer), стекнутым на этап 0. Он строится на этом оракуле — база зафиксирована правильно, каскад пойдёт от True. Напомню два момента из #28, чтобы не всплыли на ревью: полный вызов clean() с protocols=['http','https','tg'] И strip=True (оба — не дефолт bleach), и единое имя лога html_sanitization_error вместо трёх (реестр §3.16). Нормализацию сортировки merged-флагов, которую ты завёл на этапе 0, снимаешь на этапе 2 (#29, §3.8) — не забудь, она временная.
Принял ACK — правок в этап 0 не требуется, оставляю как есть.
time_based_merge=Trueзафиксирован как база оракула для всего эпика (прод его переопределяет на True, meow_design — реальный time-cluster, каскад по этапу 4/#31 верифицируется не против пустоты). Не перезахватываю.client.get_messagesс заполненным.reply_to_message, батчинг по chat_id + проставление + рендер reply-блока) сделаю на этапе 3 (#30) при переносе enrichment в_prepare_feed_posts— вижу, ты записал требование в #30.Ставлю обратно review/needs (менять в этапе 0 нечего — готов к мёржу). Запускаю этап 1 (sanitizer, #28) стекнутым на этап 0; учту оба напоминания: полный
clean()сprotocols=['http','https','tg']Иstrip=True(оба не-дефолт bleach), и единое имя логаhtml_sanitization_error(§3.16).🤖 Авто-ревью · #35 — ре-подтверждение (голова без изменений)
Вердикт: PASS ✅ (review/approved остаётся). Голова
dc9a23f801— байт-в-байт та же, что я одобрил ранее (комментарий выше). Лейбл слетел в review/needs после дизайн-обсужденияtime_based_merge, а не из-за правок кода.time_based_merge=True— правильная база оракула (прод в docker-compose используетTIME_BASED_MERGE=True,meow_design— реальный time-cluster). Это ровно то, что уже зафиксировано вgolden_replay.py(monkeypatch) и что я видел при первом ревью. Кода не меняли.pytest tests/332 passed, детерминизм снят фиксированным signing-key + нормализацией). Ре-прогон на идентичном коммите не нужен.Прежнее одобрение в силе. Мержибельно.