feat(telegram): coalesce forwarded messages (#146)

This commit is contained in:
banteg
2026-01-16 00:21:04 +04:00
committed by GitHub
parent 3f2932e76d
commit e0826ed18c
7 changed files with 541 additions and 126 deletions
+136
View File
@@ -1149,6 +1149,19 @@ def test_resolve_message_accepts_backticked_ctx_line() -> None:
assert resolved.context == RunContext(project="takopi", branch="feat/api")
def test_is_forwarded_detects_forward_fields() -> None:
assert telegram_loop._is_forwarded({"forward_origin": {"type": "user"}})
assert telegram_loop._is_forwarded({"forward_from": {"id": 1}})
assert telegram_loop._is_forwarded({"forward_from_chat": {"id": 1}})
assert telegram_loop._is_forwarded({"forward_from_message_id": 2})
assert telegram_loop._is_forwarded({"forward_sender_name": "anon"})
assert telegram_loop._is_forwarded({"forward_signature": "sig"})
assert telegram_loop._is_forwarded({"forward_date": 123})
assert telegram_loop._is_forwarded({"is_automatic_forward": True})
assert not telegram_loop._is_forwarded({"text": "hello"})
assert not telegram_loop._is_forwarded(None)
def test_topic_title_matches_command_syntax() -> None:
transport = _FakeTransport()
cfg = _make_cfg(transport)
@@ -1979,6 +1992,129 @@ async def test_run_main_loop_voice_transcript_preserves_directive(
assert codex_runner.calls[0][0].startswith("(voice transcribed) do thing")
@pytest.mark.anyio
async def test_run_main_loop_debounces_forwarded_messages_preserves_directives() -> (
None
):
codex_runner = ScriptRunner([Return(answer="codex")], engine=CODEX_ENGINE)
claude_runner = ScriptRunner([Return(answer="claude")], engine="claude")
router = AutoRouter(
entries=[
RunnerEntry(engine=claude_runner.engine, runner=claude_runner),
RunnerEntry(engine=codex_runner.engine, runner=codex_runner),
],
default_engine=claude_runner.engine,
)
runtime = TransportRuntime(router=router, projects=_empty_projects())
transport = _FakeTransport()
exec_cfg = ExecBridgeConfig(
transport=transport,
presenter=MarkdownPresenter(),
final_notify=True,
)
cfg = TelegramBridgeConfig(
bot=_FakeBot(),
runtime=runtime,
chat_id=123,
startup_msg="",
exec_cfg=exec_cfg,
)
async def poller(_cfg: TelegramBridgeConfig):
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=1,
text="/codex summarize these",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
)
await anyio.sleep(_cfg.forward_coalesce_s / 2)
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=2,
text="a",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
raw={"forward_origin": {"type": "user"}},
)
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=3,
text="b",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
raw={"forward_origin": {"type": "user"}},
)
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=4,
text="c",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
raw={"forward_origin": {"type": "user"}},
)
await run_main_loop(cfg, poller)
assert not claude_runner.calls
assert len(codex_runner.calls) == 1
prompt_text, _ = codex_runner.calls[0]
assert prompt_text == "summarize these\n\na\n\nb\n\nc"
@pytest.mark.anyio
async def test_run_main_loop_ignores_forwarded_without_prompt() -> None:
runner = ScriptRunner([Return(answer="ok")], engine=CODEX_ENGINE)
runtime = TransportRuntime(router=_make_router(runner), projects=_empty_projects())
transport = _FakeTransport()
exec_cfg = ExecBridgeConfig(
transport=transport,
presenter=MarkdownPresenter(),
final_notify=True,
)
cfg = TelegramBridgeConfig(
bot=_FakeBot(),
runtime=runtime,
chat_id=123,
startup_msg="",
exec_cfg=exec_cfg,
)
async def poller(_cfg: TelegramBridgeConfig):
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=1,
text="a",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
raw={"forward_origin": {"type": "user"}},
)
yield TelegramIncomingMessage(
transport="telegram",
chat_id=123,
message_id=2,
text="b",
reply_to_message_id=None,
reply_to_text=None,
sender_id=123,
raw={"forward_origin": {"type": "user"}},
)
await run_main_loop(cfg, poller)
assert runner.calls == []
@pytest.mark.anyio
async def test_run_main_loop_prompt_upload_auto_resumes_chat_sessions(
tmp_path: Path,