From f2b09fbc7aded31d6819c11671b95dd9d4fe309e Mon Sep 17 00:00:00 2001 From: Flexlug Date: Sun, 26 Jul 2026 02:54:36 +0300 Subject: [PATCH] =?UTF-8?q?test(wiring):=20fan-out=20RX=20=D0=B8=D0=B7=20L?= =?UTF-8?q?oRa=20=D0=B2=20=D0=BE=D0=B1=D0=B0=20Telegram-=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=BE=D0=BC=D0=BD=D0=B0=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Закрывает пробел покрытия: LoRa→TG тестировался только с одним подписчиком. Два теста на _CFG_TWO_TG_SUBS: зеркало приходит в оба чата; падение зеркала в один чат не мешает доставке во второй (per-member глотание исключений в Bridge.mirror_to_messenger). Во втором тесте journal.stop() в finally: при регрессии глотания исключение вылетает из task group, и незакрытый aiosqlite (не-daemon поток) вешал бы pytest на выходе вместо красного отчёта. beads: LoRa-Bridge-hox (закрыт), follow-up LoRa-Bridge-bas, LoRa-Bridge-la9 Co-Authored-By: Claude Fable 5 --- tests/test_wiring_integration.py | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_wiring_integration.py b/tests/test_wiring_integration.py index b38dfc4..283d18e 100644 --- a/tests/test_wiring_integration.py +++ b/tests/test_wiring_integration.py @@ -725,6 +725,71 @@ async def test_mirror_to_second_subscriber_after_commit(wire_fakes): assert "-100" not in sent_chats # в источник не возвращается +async def test_lora_rx_mirrors_to_both_tg_chats(wire_fakes): + """RX из LoRa зеркалится в оба Telegram-чата комнаты (fan-out, §12.1).""" + lora_fakes, tg_fakes = wire_fakes + bridge, _, journal = await assemble(_CFG_TWO_TG_SUBS) + + incoming = Message( + id="l1", + source=ChannelRef("mc-1", "general"), + sender=Identity(display_name="Bob", transport_uid="b1"), + text="из эфира", + ) + + async with anyio.create_task_group() as tg: + tg.start_soon(bridge.consume, lora_fakes["mc-1"]) + await anyio.sleep(0) # даём consume() подписаться на hub + await lora_fakes["mc-1"].inject(incoming) + await anyio.sleep(0) # даём consume() обработать + tg.cancel_scope.cancel() + + await journal.stop() + + sent_chats = sorted(ref.channel for ref, _ in tg_fakes["tg"].sent) + assert sent_chats == ["-100", "-200"] # ровно по одному зеркалу в каждый чат + assert all("из эфира" in m.text for _, m in tg_fakes["tg"].sent) + + +async def test_mirror_failure_does_not_block_second_chat(wire_fakes, monkeypatch): + """Падение зеркала в один чат не мешает доставке во второй (исключения глотаются per-member).""" + lora_fakes, tg_fakes = wire_fakes + bridge, _, journal = await assemble(_CFG_TWO_TG_SUBS) + + tg_fake = tg_fakes["tg"] + real_send = tg_fake.send + + async def send_failing_first_chat(target, msg): + # "-100" в комнате первый: падение происходит ДО зеркала во второй чат + if target.channel == "-100": + raise RuntimeError("чат недоступен") + return await real_send(target, msg) + + monkeypatch.setattr(tg_fake, "send", send_failing_first_chat) + + incoming = Message( + id="l1", + source=ChannelRef("mc-1", "general"), + sender=Identity(display_name="Bob", transport_uid="b1"), + text="из эфира", + ) + + # journal.stop() в finally: если регрессия перестанет глотать исключение, оно вылетит + # из task group, а незакрытый aiosqlite (не-daemon поток) повесил бы pytest на выходе + try: + async with anyio.create_task_group() as tg: + tg.start_soon(bridge.consume, lora_fakes["mc-1"]) + await anyio.sleep(0) + await lora_fakes["mc-1"].inject(incoming) + await anyio.sleep(0) + tg.cancel_scope.cancel() + finally: + await journal.stop() + + sent_chats = [ref.channel for ref, _ in tg_fakes["tg"].sent] + assert sent_chats == ["-200"] # второй чат получил зеркало несмотря на падение первого + + async def test_label_never_omits_type_prefix(wire_fakes): """include_type: never → [Alex] вместо [TG:Alex] даже при двух мессенджерах в комнате.""" lora_fakes, _ = wire_fakes