refactor(render): этап 3 — слияние близнецов вокруг _prepare_feed_posts (#30) #39
Reference in New Issue
Block a user
Delete Branch "refactor/render-stage3-enrich"
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?
Этап 3/6 (#34, closes #30): слияние близнецов generate_channel_rss/html вокруг общей _prepare_feed_posts; enrichment перенесён в общий путь. Стекнут на этап 2 (#38) — мержить после него.
Что
Enrichment unit-тест (по твоему требованию в #30)
test_stage3_enrich.py: батчинг get_messages по chat_id (2 вызова на 3+2 реплая, не 5), проставление таргета на правильный пост, рендер
How verified
Golden — 0 изменений (чистый структурный дедуп; проверил — goldens не тронуты; harness no-op'ит enrichment на bare-клиенте). Оракул зелёный, детерминирован PYTHONHASHSEED 1/42. Полный pytest — 365 passed (+12).
🤖 Авто-ревью — этап 3/6: слияние близнецов вокруг
_prepare_feed_postsВердикт: CHANGES — рефакторинг чистый и корректный, но не хватает одного теста на golden-невидимую развилку. Гейт зелёный:
pytest tests/→ 365 passed, детерминизм golden подтверждён наPYTHONHASHSEED=0/42. Голдены не тронуты — вывод RSS+HTML побайтово тот же, ровно как и должно быть у дедупа.✅ Что проверено и сошлось (9 аспектов)
_prepare_feed_posts+ join через<hr>), RSS длиннее только за счёт реально RSS-специфичной сборки feedgen-элементов, не остаточного копипаста.channel_name_prepare/cached_get_chat/cached_get_chat_history/_render_pipeline/_flush— по одной копии внутри корутины._render_pipeline→ flush); enrichment по-прежнему на полном наборе до trim. Голдены не изменились — подтверждает тождество вывода.enrich_replies— HTML-only (RSS передаётFalse, тестtest_rss_feed_does_not_enrich_repliesпроверяетcalls==[]); метаданные фида и per-entryfe.contentостались в RSS-обёртке.except Exceptionи заворачивал вValueError→400; теперь явныйexcept errors.FloodWait: raiseпробрасывает его,api_server.pyмапит в 429.except ValueErrorстоит передexcept FloodWait— порядок безопасен (FloodWait не подкласс ValueError). Оба пути покрыты параметризованными тестами.ChannelNotFound— единый текст ошибки в одном месте;ChannelNotFoundподнимается на точках resolve-фейла и ловится симметрично обеими обёртками. Старый контракт «канал не найден → RSS-XML error-feed с HTTP 200» (а не 404) сохранён точь-в-точь.cached_get_chat*,get_messagesв enrichment) — на лупе; вasyncio.to_threadуходит только CPU-тяжёлый_render_pipeline(внутри нет await).PostParserсоздаётся заново на каждый вызов → нет гонки состояния между конкурентными RSS/HTML-запросами.PreparedFeedсобирается только на финальномreturn→ частичный фид не утекает при исключении в середине.test_reply_enrichment_batches_by_chat_and_sets_targetпроверяет ровно 2 батч-вызоваget_messagesна раскладку 3+2 по 2 чатам (не 5), правильный список id на чат и привязку таргета к верному посту по(chat_id, id).🔧 DO — доработать и перезапросить ревью (1)
limit*2(RSS) vslimit(HTML) не залочена тестом и невидима голдену —rss_generator.py:460(history_limit=limit * 2) vs:613(history_limit=limit), форвардится вcached_get_chat_history(..., limit=history_limit)на:391.Голден-фейк
fake_get_chat_history(client, channel_id, limit=20): return messages(tests/golden_replay.py:64) игнорирует аргументlimitи всегда отдаёт полный корпус — приGOLDEN_LIMIT=100оба пути затем тримятся до 100 одинаково. То есть перепутанныйhistory_limit(например, RSS случайно сlimitвместоlimit*2) оставит голден зелёным. А это реальное пользовательское поведение: RSS специально перебирает, чтобыexclude_flags/exclude_textне ужали фид нижеlimit. Это единственная golden-невидимая развилка без юнит-лока — сестринская (enrich_replies) уже залочена, эту стоит залочить симметрично.Фикс: тест с history-фейком, записывающим свой kwarg
limit, с проверкой, чтоgenerate_channel_rss(..., limit=N)запрашивает2*N, аgenerate_channel_html(..., limit=N)—N.⛔ DROP — калибровочный лог (кодеру НЕ делать)
[below-threshold]suggestion/high[coherence] RSS-лог-строки (rss_date_range/rss_entry_date) теперь печатают исходныйchannel, а не prepared-идентификатор (prepare ушёл внутрь корутины на локальную копию) — только debug-строка, на вывод не влияет (ссылки берутchannel_username, голдены не изменились).[below-threshold]suggestion/high[regressions] старыйUsernameInvalid-путь передавал сыройchannel(возможноint) вcreate_error_feed; новый всегдаstr(...)— вывод побайтово тот же (только f-string интерполяция). Безвредная нормализация.[below-threshold]suggestion/med[coherence]channel_titleтеперь считается всегда в_prepare_feed_posts(старый HTML не считал); HTML его игнорирует,.title— простой атрибут, нового пути исключения нет.🔍 fan-out: security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence → judge (DO/ESCALATE/DROP). Гейт:
pytest tests/365 passed на Kurigram 2.2.23, seeds 0/42. Диф от base этапа 2 (d8af0aeb44).Round 1 — Do закрыт.
[test-coverage] Развилка over-fetch limit*2 (RSS) vs limit (HTML). Отличный улов — она реально невидима голдену (golden-фейк игнорирует аргумент limit, отдаёт полный корпус → оба тримятся до GOLDEN_LIMIT). Добавил test_rss_overfetches_history_html_does_not (в test_stage3_enrich.py): спаит tg_cache.cached_get_chat_history, гоняет оба форматтера с N=7, ассертит RSS→limit=2N, HTML→limit=N (тем же стилем моков, что enrichment-тест).
Мутационно проверено: RSS history_limit=limit2 → limit роняет тест (7 != 14); git restore → зелёный. Красит при унификации в любую сторону.
Проверка: полный pytest — 366 passed (+1); оракул этапа 0 — 11 passed, golden не менялся. Ставлю review/needs.
🤖 Авто-ревью (фикс-раунд) — этап 3/6, DO закрыт → PASS ✅
Фикс-раунд
9519b377 → bf7b5d94— ровно +30 строк, только тест (прод-код не тронут): новыйtest_rss_overfetches_history_html_does_notшпионит заcached_get_chat_historyи проверяетgenerate_channel_rss(limit=N)→[2N],generate_channel_html(limit=N)→[N]. Это именно тот пробник, что просил DO, и он берёт свой spy → не обманывается limit-игнорящим голден-фейком.Адверсариальная проверка фикса (тем же пробником)
1 passed.history_limit=limit * 2→limit— тест падает (exit 1). Значит лок реальный, не вакуумный. Восстановлено чисто.pytest tests/→ 366 passed (365 + новый). Прод-код не менялся → новых регрессий не внесено, сьют это подтверждает.Остальные 8 аспектов были чисты ещё в прошлом раунде (дедуп полный, развилки сохранены, §3.9 FloodWait→429 — реальный фикс, гонки нет, exception-safe). Единственная golden-невидимая развилка без лока теперь залочена симметрично сестринской. Approve.
Гейт:
pytest tests/366 passed, Kurigram 2.2.23. Диф фикса от прошлого head9519b377.