Развилка history_limit=limit*2 (RSS) vs limit (HTML) невидима голдену (golden-фейк
игнорирует аргумент limit и всегда отдаёт полный корпус → оба пути тримятся до
GOLDEN_LIMIT одинаково). test_rss_overfetches_history_html_does_not спаит
tg_cache.cached_get_chat_history, гоняет оба форматтера с N=7 и ассертит:
RSS форвардит limit=2*N, HTML форвардит limit=N. Мутационно проверено — красит
при унификации в любую сторону. Полный сюит 366 passed, golden не менялся.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
~80% дублированных тел generate_channel_rss / generate_channel_html сведены в
одну корутину _prepare_feed_posts (validate limit → cached_get_chat →
cached_get_chat_history → опц. _reply_enrichment → to_thread(_render_pipeline)
с _flush_pending_media_ids в finally). channel_name_prepare/cached_get_chat/
cached_get_chat_history/_render_pipeline/_flush_pending_media_ids теперь по
одной копии (было по две). Новые @dataclass PreparedFeed + ChannelNotFound
(→ create_error_feed, байт-эквивалентно). Форматтеры тонкие: RSS
(history_limit=limit*2, enrich_replies=False), HTML (limit, enrich_replies=True).
_reply_enrichment (живой client.get_messages) перенесён в _prepare_feed_posts
(гейт enrich_replies, HTML-only; RSS намеренно пропускает). §3.9 FloodWait из
истории ре-райзится (→429) вместо ValueError (→400); §3.10 тексты ValueError
унифицированы.
Enrichment закрыт unit-тестом (по требованию мейнтейнера в #30): батчинг по
chat_id (2 вызова get_messages на 3+2 реплая, не 5), проставление таргета на
правильный пост, рендер <div class="message-reply"> end-to-end; плюс
RSS-не-обогащает и error-path тесты обоих форматтеров.
Golden — БЕЗ изменений (чистый структурный дедуп; harness no-op'ит enrichment
на bare-клиенте). Оракул зелёный и детерминирован (PYTHONHASHSEED 1/42).
Полный сюит 365 passed (+12).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>