diff --git a/post_parser.py b/post_parser.py index db15bd9..bc08055 100644 --- a/post_parser.py +++ b/post_parser.py @@ -1055,7 +1055,12 @@ class PostParser: else: emoji = "❓" # Default for unknown cases reactions_html += f'{emoji} {reaction.count}  ' reactions_html = reactions_html.rstrip() - first_line_parts.append(reactions_html) + # An empty reactions object (reactions.reactions == []) produces no + # spans; appending the empty string would emit a leading + # '  |  ' separator in the footer. Skip it + # (registry §3.15). Affects single posts too. + if reactions_html: + first_line_parts.append(reactions_html) # Add views if views := getattr(message, "views", None): diff --git a/rss_generator.py b/rss_generator.py index 1344e4d..80aa7ec 100644 --- a/rss_generator.py +++ b/rss_generator.py @@ -15,7 +15,6 @@ import asyncio import re import time from datetime import datetime, timezone -from types import SimpleNamespace from typing import Optional from feedgen.feed import FeedGenerator from pyrogram import errors, Client @@ -151,54 +150,6 @@ def _trim_messages_groups(messages_groups: list[list[Message]], limit: int): return messages_groups -def processed_message_to_tg_message(processed_message: dict) -> Message: - """ - Convert processed message dictionary into a Message-like object - containing only the attributes needed by generate_html_footer. - """ - # Create a simple chat object - chat_info = SimpleNamespace() - channel_identifier = processed_message.get('channel') - if isinstance(channel_identifier, str) and channel_identifier.startswith('-100'): - setattr(chat_info, 'id', int(channel_identifier)) - setattr(chat_info, 'username', None) - else: - setattr(chat_info, 'id', None) # Or some placeholder if needed - setattr(chat_info, 'username', channel_identifier) - - - # Convert reactions dict to list of objects - reactions_list = [] - if reactions_dict := processed_message.get('reactions'): - for emoji, count in reactions_dict.items(): - # Assuming no custom/paid reactions in this simplified structure - reactions_list.append(SimpleNamespace(emoji=emoji, count=count, is_paid=False, custom_emoji_id=None)) - - # Recreate reactions structure expected by Pyrogram's reaction handling - reactions_obj = SimpleNamespace(reactions=reactions_list) if reactions_list else None - - # Create the message-like object - tg_message_mock = SimpleNamespace( - id=processed_message.get('message_id'), - date=datetime.fromtimestamp(processed_message['date'], tz=timezone.utc) if processed_message.get('date') else None, - views=processed_message.get('views'), - reactions=reactions_obj, - chat=chat_info, - # Add other attributes if generate_html_footer or its dependencies need them - # For now, these seem sufficient based on the analysis of generate_html_footer - # and _reactions_views_links. - text=processed_message.get('text'), # Add text just in case - caption=None, # Assume caption is merged into text by process_message - forward_origin=None, # Not directly needed by footer generation logic itself - reply_to_message=None, # Not directly needed by footer generation logic itself - media=None, # Not needed by footer - service=processed_message.get('service') # Potentially needed? Added just in case. - ) - - # Cast to Message type hint for static analysis, although it's a mock object - return tg_message_mock # type: ignore - - def _render_messages_groups(messages_groups: list[list[Message]], post_parser: PostParser, exclude_flags: str | None = None, @@ -240,12 +191,14 @@ def _render_messages_groups(messages_groups: list[list[Message]], else: # Multiple messages in group - merge text and html body processed_messages = [post_parser.process_message(msg, include_raw=False, sanitize=False) for msg in group] - # Determine main message for header/footer/title - main_message = next( - (msg for msg in processed_messages if msg['text']), - processed_messages[0] # fallback if no message contains text - ) - + # Determine the main message with the SAME criterion the processed dicts + # used: first message that has text or caption, else the first of the + # group. processed_messages[i] corresponds to group[i], so main_message + # (dict, for title/date/author) and main_raw (the real Message, for the + # footer) point at the same index. + main_idx = next((i for i, m in enumerate(group) if (m.text or m.caption)), 0) + main_raw = group[main_idx] + main_message = processed_messages[main_idx] # Merge text fields from all messages all_texts = [msg['text'] for msg in processed_messages if msg['text']] @@ -255,19 +208,16 @@ def _render_messages_groups(messages_groups: list[list[Message]], all_html_bodies = [msg['html']['body'] for msg in processed_messages if msg['html']['body']] combined_html_body = '\n

\n'.join(all_html_bodies) - # Collect all unique flags from all messages in the group - all_flags = set() - for msg in processed_messages: - if msg.get('flags'): # Check if flags exist and are not empty - all_flags.update(msg['flags']) - all_flags.add("merged") - merged_flags = list(all_flags) # Convert back to list if needed, or keep as set + # Deterministic merged flags: first-seen order across the group, then + # 'merged' (registry §3.8 — replaces the hash-ordered list(set(...))). + merged_flags = list(dict.fromkeys(f for msg in processed_messages for f in msg['flags'])) + merged_flags.append("merged") - # generate tg-message from processed message - tg_message = processed_message_to_tg_message(main_message) - - - footer_html = post_parser.generate_html_footer(tg_message, flags_list=merged_flags) + # Render the merged footer DIRECTLY from the real main Message — no + # dict->mock round-trip. The raw Message carries its real reactions + # (custom emojis get their own span, registry §3.6) and its naive-local + # date (registry §3.7), matching a single post of the same message. + footer_html = post_parser.generate_html_footer(main_raw, flags_list=merged_flags) html_parts = [ f'
{combined_html_body}
', diff --git a/tests/golden_replay.py b/tests/golden_replay.py index ccedda4..d66dd88 100644 --- a/tests/golden_replay.py +++ b/tests/golden_replay.py @@ -118,27 +118,21 @@ def capture_html(channel): _LASTBUILDDATE_RE = re.compile(r".*?", re.DOTALL) # feedgen 1.0.0 emits no ; normalized anyway as cheap insurance vs. a lib upgrade. _GENERATOR_RE = re.compile(r".*?", re.DOTALL) -# Merged-post flags are built as list(set(...)) (rss_generator.py:260-265): order depends on -# PYTHONHASHSEED, which conftest cannot pin. Sort the flag tokens inside each flags div. -# (This normalization is removed by the stage-2 commit referencing §3.8.) -_FLAGS_DIV_RE = re.compile(r'
(.*?)
', re.DOTALL) - - -def _sort_flags_div(match): - flags = sorted(p.strip() for p in match.group(1).split("🏷") if p.strip()) - inner = " ".join("🏷 " + f for f in flags) - return f'
{inner}
' +# NOTE: the stage-0 flag-sort normalization (_FLAGS_DIV_RE / _sort_flags_div) was +# removed in stage 2 (§3.8). Merged-post flags are now built in deterministic +# first-seen order (rss_generator._render_messages_groups: dict.fromkeys(...)), +# so the golden stores the real order and no normalization is needed — keeping it +# would only mask a real flag-order regression. def normalize_rss(xml): xml = _LASTBUILDDATE_RE.sub("", xml) xml = _GENERATOR_RE.sub("", xml) - xml = _FLAGS_DIV_RE.sub(_sort_flags_div, xml) return xml def normalize_html(html): - return _FLAGS_DIV_RE.sub(_sort_flags_div, html) + return html def golden_path(channel, kind): diff --git a/tests/test_data/golden/bladerunnerblues.feed.html b/tests/test_data/golden/bladerunnerblues.feed.html index bc51217..94063fb 100644 --- a/tests/test_data/golden/bladerunnerblues.feed.html +++ b/tests/test_data/golden/bladerunnerblues.feed.html @@ -154,7 +154,7 @@
+
🏷 no_image 🏷 fwd 🏷 link 🏷 mention 🏷 hid_channel 🏷 foreign_channel 🏷 merged

@@ -419,7 +419,7 @@

+
🏷 clownpoo 🏷 link 🏷 merged

diff --git a/tests/test_data/golden/bladerunnerblues.rss.xml b/tests/test_data/golden/bladerunnerblues.rss.xml index b10e711..8762c20 100644 --- a/tests/test_data/golden/bladerunnerblues.rss.xml +++ b/tests/test_data/golden/bladerunnerblues.rss.xml @@ -7,7 +7,7 @@ http://www.rssboard.org/rss-specification python-feedgen ru - Mon, 06 Jul 2026 04:36:26 +0000 + Mon, 06 Jul 2026 14:33:40 +0000 Дорогой экипаж! https://t.me/bladerunnerblues/295 @@ -1089,7 +1089,7 @@

]]> +
🏷 clownpoo 🏷 link 🏷 merged
]]> https://t.me/bladerunnerblues/362 Fri, 09 May 2025 09:44:44 +0000 @@ -1441,7 +1441,7 @@
]]> +
🏷 no_image 🏷 fwd 🏷 link 🏷 mention 🏷 hid_channel 🏷 foreign_channel 🏷 merged
]]> https://t.me/bladerunnerblues/387 Mon, 23 Mar 2026 12:55:02 +0000 diff --git a/tests/test_data/golden/embedoka.feed.html b/tests/test_data/golden/embedoka.feed.html index 82ab385..fc0e518 100644 --- a/tests/test_data/golden/embedoka.feed.html +++ b/tests/test_data/golden/embedoka.feed.html @@ -63,7 +63,7 @@
+
🏷 link 🏷 mention 🏷 merged

@@ -113,8 +113,8 @@

+⭐ 1  🔥 13  ❓ 4  😁 3  ❓ 2  ❤ 1  👍 1    |  3365 👁  |  10/04/26, 09:13:01  |  #1428
Open in Telegram | Open in Web +
🏷 link 🏷 mention 🏷 merged

@@ -306,7 +306,7 @@

+
🏷 link 🏷 mention 🏷 merged

@@ -419,7 +419,7 @@

+
🏷 link 🏷 mention 🏷 merged

--- Forwarded from Embedded Doka (@embedoka) ---
@@ -768,7 +768,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo

+
🏷 link 🏷 mention 🏷 merged

@@ -837,7 +837,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo
📊 Poll: Стоит ли выкладывать копии релевантных видео в канал (для участников, у которых ограничен доступ к ютьюб)? И вообще дублировать какой-либо контент в тг, доступ к которому может быть ограничен?
1. да
2. нет
3. всё равно
4. напишу в комментариях

→ Vote in Telegram 🔗


@@ -984,7 +984,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo

+
🏷 link 🏷 mention 🏷 merged

@@ -1084,7 +1084,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo

+
🏷 link 🏷 mention 🏷 merged

DIY rule #43:

diff --git a/tests/test_data/golden/embedoka.rss.xml b/tests/test_data/golden/embedoka.rss.xml index 89d6efe..7a20ade 100644 --- a/tests/test_data/golden/embedoka.rss.xml +++ b/tests/test_data/golden/embedoka.rss.xml @@ -7,7 +7,7 @@ http://www.rssboard.org/rss-specification python-feedgen ru - Mon, 06 Jul 2026 04:36:27 +0000 + Mon, 06 Jul 2026 14:33:41 +0000 Интересно при чтении нехудожественных книг... https://t.me/embedoka/1337 @@ -114,7 +114,7 @@

]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1341 Thu, 06 Mar 2025 18:05:55 +0000 @@ -236,7 +236,7 @@
]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1350 Thu, 10 Apr 2025 08:45:12 +0000 @@ -434,7 +434,7 @@
📊 Poll: Стоит ли выкладывать копии релевантных видео в канал (для участников, у которых ограничен доступ к ютьюб)? И вообще дублировать какой-либо контент в тг, доступ к которому может быть ограничен?
1. да
2. нет
3. всё равно
4. напишу в комментариях

→ Vote in Telegram 🔗

]]> https://t.me/embedoka/1363 Mon, 05 May 2025 17:21:14 +0000 @@ -559,7 +559,7 @@
]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1370 Thu, 22 May 2025 07:49:49 +0000 @@ -1097,7 +1097,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo
]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1400 Thu, 14 Aug 2025 08:10:58 +0000 @@ -1218,7 +1218,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo
]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1411 Tue, 02 Sep 2025 10:40:26 +0000 @@ -1506,8 +1506,8 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo

]]> +⭐ 1  🔥 13  ❓ 4  😁 3  ❓ 2  ❤ 1  👍 1    |  3365 👁  |  10/04/26, 09:13:01  |  #1428
Open in Telegram | Open in Web +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1428 Fri, 10 Apr 2026 09:13:01 +0000 @@ -1569,7 +1569,7 @@ The SM9 module boasts hardware compatibility with NVIDIA Jetson series mo
]]> +
🏷 link 🏷 mention 🏷 merged
]]> https://t.me/embedoka/1433 Mon, 04 May 2026 14:04:48 +0000 diff --git a/tests/test_data/golden/meow_design.feed.html b/tests/test_data/golden/meow_design.feed.html index 62ef411..51b44c0 100644 --- a/tests/test_data/golden/meow_design.feed.html +++ b/tests/test_data/golden/meow_design.feed.html @@ -14,7 +14,7 @@
+
🏷 no_image 🏷 link 🏷 merged

Разрешающая способность

У каждого из нас есть своя разрешающая способность, различная от области к области:

— моя жена отличит плохое исполнение оперной арии от хорошего, для меня же оба будут лежать рядом на полочке «так поют в опере»

— орнитолог умеет распознавать виды птиц, которые для большинства людей — неотличимые «маленькые серые птички»

— кто-то даже по форме светящихся ночью фар способен определить марку и модель авто, для других же машинки отличаются лишь цветом и размером

— гроссмейстер видит на шахматной доске то, что не видит новичок

— ценители кофе третьей волны ловят нотки послевкусия и умеют отличать Эфиопию от Кении, для других же весь кофе одинаковый на вкус (будто горсть земли залили кипятком)

Этот список очевидных примеров можно продолжать бесконечно. Думаю, у каждого из нас есть область, в которой мы видим различия, нераспознаваемые большинством других людей. Наша разрешающая способность в этой области выше. Ну вы знаете, Фрукт-фрукт, сиська-сиська. They're The Same Picture точка жпг

Это как набор рецепторов, по одному на каждую область. Они имеют определенную чувствительность, примерно как улавливающий свет фоторецептор.

0. На нулевом уровне рецептор слепой и ни на что не способен. Как если дать мне искать пневмонию на рентгеновском снимке грудной клетки.

1. На первом уровне рецептор однобитный, он может отвечать только «да-нет», есть свет или нет. Хороший фильм или плохой, вкусный кофе или нет.

2. На втором уровне рецептор начинает различать уровни интенсивности сигнала. Вместо бинарного «плохо-хорошо» появляется шкала от «очень плохо» до «очень хорошо».

3. На третьем уровне рецептор выходит за пределы одной шкалы и уже способен раскладывать сигнал по нескольким измерениям, зрение становится цветным.

Вместо «классный логотип»«логотип современный и даже пытается быть в тренде из-за использования гипертрофированных ловушек для краски; спрятанная в форме метафора остроумная и узконаправленная, ведь ее распознают только те кто в теме; форма динамичная, общая серьезность оттеняется игривостью в деталях».

Вместо «средний футболист»«умный и техничный опорник с хорошим ударом, но при этом медленный и ленивый, плохо играет головой, склонен к излишнему риску»

Вместо «мерзавец»«прекрасный музыкант с природным актерским талантом и харизмой, но паталогический лжец и приспособленец, готовый идти по головам ради славы»

Дальнейшее повышение разрешающей способности идет через увеличение битности шкал и добавление всё новых измерений. В том числе — добавление оси времени, то есть способность распознавать изменение сигнала со временем: «раньше это обладало свойствами А, а теперь уже А*» @@ -32,7 +32,7 @@

+
🏷 no_image 🏷 link 🏷 merged

@@ -139,7 +139,7 @@

+
🏷 no_image 🏷 link 🏷 merged

@@ -219,7 +219,7 @@

+
🏷 no_image 🏷 link 🏷 merged

Меня пожурили, что я на канале кормлю семечками «а-ха-ха, какой-то дизайнер лоханулся» вместо открытия каких-то глубинных глубин дизайна.

После этого я подвис больше чем на месяц, так как постить всё как обычно было уже как-то неловко, а глубинные глубины требуют время на созревание формулировок (если только не делегировать графоманию гптшкам). @@ -291,7 +291,7 @@

+
🏷 no_image 🏷 link 🏷 merged

@@ -392,7 +392,7 @@

+
🏷 no_image 🏷 link 🏷 merged

@@ -517,7 +517,7 @@

+
🏷 no_image 🏷 poll 🏷 merged

@@ -605,7 +605,7 @@

+
🏷 no_image 🏷 link 🏷 merged

@@ -623,7 +623,7 @@

+
🏷 fwd 🏷 no_image 🏷 link 🏷 mention 🏷 foreign_channel 🏷 merged

diff --git a/tests/test_data/golden/meow_design.rss.xml b/tests/test_data/golden/meow_design.rss.xml index 22787c4..8ec966f 100644 --- a/tests/test_data/golden/meow_design.rss.xml +++ b/tests/test_data/golden/meow_design.rss.xml @@ -7,7 +7,7 @@ http://www.rssboard.org/rss-specification python-feedgen ru - Mon, 06 Jul 2026 04:36:28 +0000 + Mon, 06 Jul 2026 14:33:42 +0000 Мы уже давно ушли из той эпохи веб-дизайна, где все... https://t.me/meow_design/2158 @@ -325,7 +325,7 @@

]]> +
🏷 fwd 🏷 no_image 🏷 link 🏷 mention 🏷 foreign_channel 🏷 merged
]]> https://t.me/meow_design/2183 Mon, 22 Dec 2025 15:22:09 +0000 @@ -344,7 +344,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2185 Mon, 22 Dec 2025 15:22:40 +0000 @@ -447,7 +447,7 @@
]]> +
🏷 no_image 🏷 poll 🏷 merged
]]> https://t.me/meow_design/2195 Fri, 02 Jan 2026 15:48:45 +0000 @@ -620,7 +620,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2210 Thu, 05 Feb 2026 18:13:58 +0000 @@ -789,7 +789,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2222 Sun, 19 Apr 2026 11:55:56 +0000 @@ -839,7 +839,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2233 Mon, 20 Apr 2026 19:48:52 +0000 @@ -972,7 +972,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2241 Thu, 14 May 2026 13:10:50 +0000 @@ -1042,7 +1042,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2257 Wed, 03 Jun 2026 22:18:58 +0000 @@ -1066,7 +1066,7 @@
]]> +
🏷 no_image 🏷 link 🏷 merged
]]> https://t.me/meow_design/2260 Wed, 24 Jun 2026 02:29:30 +0000 diff --git a/tests/test_data/golden/theyforcedme.feed.html b/tests/test_data/golden/theyforcedme.feed.html index 96e73ff..d9233e8 100644 --- a/tests/test_data/golden/theyforcedme.feed.html +++ b/tests/test_data/golden/theyforcedme.feed.html @@ -235,7 +235,7 @@
+
🏷 audio 🏷 link 🏷 no_image 🏷 merged

@@ -344,7 +344,7 @@ growing up is overrated!!

my girlfriends prepared an easter egg hunt for

+
🏷 fwd 🏷 link 🏷 mention 🏷 foreign_channel 🏷 no_image 🏷 merged

@@ -1052,4 +1052,4 @@ That is completely untrue. This is not how I close a bag of chips.

\ No newline at end of file +
🏷 link 🏷 video 🏷 merged
\ No newline at end of file diff --git a/tests/test_data/golden/theyforcedme.rss.xml b/tests/test_data/golden/theyforcedme.rss.xml index df656f0..02baea4 100644 --- a/tests/test_data/golden/theyforcedme.rss.xml +++ b/tests/test_data/golden/theyforcedme.rss.xml @@ -7,7 +7,7 @@ http://www.rssboard.org/rss-specification python-feedgen ru - Mon, 06 Jul 2026 04:36:28 +0000 + Mon, 06 Jul 2026 14:33:43 +0000 Читаем кошку https://t.me/theyforcedme/3483 @@ -29,7 +29,7 @@
]]> +
🏷 link 🏷 video 🏷 merged
]]> https://t.me/theyforcedme/3483 Mon, 16 Jan 2023 20:46:53 +0000
@@ -1111,7 +1111,7 @@ growing up is overrated!!

my girlfriends prepared an easter egg hunt for
]]> +
🏷 fwd 🏷 link 🏷 mention 🏷 foreign_channel 🏷 no_image 🏷 merged
]]> https://t.me/theyforcedme/3549 Sun, 09 Apr 2023 14:06:27 +0000 @@ -1245,7 +1245,7 @@ Grammarly, ты очень хорошо корректируешь имейлы,
]]> +
🏷 audio 🏷 link 🏷 no_image 🏷 merged
]]> https://t.me/theyforcedme/3561 Mon, 17 Apr 2023 17:13:03 +0000 diff --git a/tests/test_stage0_golden.py b/tests/test_stage0_golden.py index 1b87f4c..1df3384 100644 --- a/tests/test_stage0_golden.py +++ b/tests/test_stage0_golden.py @@ -3,9 +3,10 @@ """Stage-0 golden oracle (render-pipeline refactor epic, issue #27/#34). Regenerates the RSS + HTML feeds for the frozen recorded corpus and asserts -byte-equality against the committed goldens after the spec's declared normalizations -(strip volatile , sort the hash-ordered merged-flags div). Any other -byte change = a render regression, and this test must catch it. +byte-equality against the committed goldens after the spec's declared normalization +(strip volatile ). Any other byte change = a render regression, and +this test must catch it. (The stage-0 merged-flags sort normalization was removed in +stage 2: flag order is now deterministic first-seen order — §3.8.) Guardrails: this stage only ADDS a loader + goldens + this test. No render/pipeline production code is touched; the goldens freeze CURRENT behavior including known bugs @@ -60,14 +61,14 @@ def test_all_goldens_present_and_nonempty(): assert os.path.getsize(path) > 0, f"empty golden: {path}" -def test_normalization_sorts_merged_flags(): - """Guard the load-bearing flag-sort normalization itself: merged-post flags are emitted - as list(set(...)) (hash-ordered), so the normalizer must canonicalize their order.""" - unsorted = '
🏷 video 🏷 fwd 🏷 link
' - resorted = '
🏷 link 🏷 fwd 🏷 video
' - assert gr.normalize_html(unsorted) == gr.normalize_html(resorted) - # ...and it is not a no-op that would let a real reordering slip through undetected. - assert gr.normalize_html(unsorted) == '
🏷 fwd 🏷 link 🏷 video
' +def test_normalization_preserves_flag_order(): + """Stage 2 (§3.8): merged-post flags are now emitted in deterministic first-seen + order, so normalize_html no longer reorders the flags div — a real flag reordering + must reach the byte comparison instead of being masked.""" + a = '
🏷 video 🏷 fwd 🏷 link
' + b = '
🏷 link 🏷 fwd 🏷 video
' + assert gr.normalize_html(a) != gr.normalize_html(b) + assert gr.normalize_html(a) == a def test_normalization_strips_lastbuilddate(): diff --git a/tests/test_stage2_footer.py b/tests/test_stage2_footer.py new file mode 100644 index 0000000..0982f2e --- /dev/null +++ b/tests/test_stage2_footer.py @@ -0,0 +1,188 @@ +# flake8: noqa +# pylint: disable=missing-function-docstring, redefined-outer-name, line-too-long +# pylint: disable=protected-access, wrong-import-position, import-outside-toplevel +"""Stage-2 footer tests (render-pipeline refactor epic, issue #29/#34). + +Stage 2 removed rss_generator.processed_message_to_tg_message and renders the merged +footer DIRECTLY from the real main Message. These tests lock the registry §3 items that +become visible as a consequence: + + §3.6 custom-emoji reactions in the merged footer get a separate "❓ N" span each + (no more aggregation into one "❓ N"), matching single posts. + §3.7 the merged footer date comes from the naive-local date of the real Message, + not a UTC mock — verified with a NON-UTC TZ where the delta is visible (the + TZ=UTC golden cannot see it). + §3.15 an empty reactions object no longer emits a leading footer separator (single + posts too). + +Messages are built with the shared SimpleNamespace helper (naive dates where relevant, +as kurigram emits on prod). +""" +import os +import time +from datetime import datetime +from types import SimpleNamespace + +import pytest + +from pyrogram.enums import MessageMediaType +from post_parser import PostParser +from rss_generator import _render_messages_groups +from tests.test_stage4_eventloop import make_message + + +def _custom_reaction(count, custom_emoji_id): + # No `.emoji` attribute -> _reactions_views_links falls to the custom-emoji "❓" branch. + return SimpleNamespace(count=count, custom_emoji_id=custom_emoji_id) + + +def _normal_reaction(count, emoji): + return SimpleNamespace(count=count, emoji=emoji) + + +def _reactions(*items): + return SimpleNamespace(reactions=list(items)) + + +def _footer_of(post): + """Extract the inner html from a rendered post.""" + html = post["html"] + marker = '