From 6c0c62595278866bd1094a4cd55c809b084bbfc6 Mon Sep 17 00:00:00 2001 From: JackJin <1037461232@qq.com> Date: Sun, 19 Apr 2026 23:33:43 +0800 Subject: [PATCH] fix(gateway): accept finalize kwarg in all platform edit_message overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stream_consumer._send_or_edit unconditionally passes finalize= to adapter.edit_message(), but only DingTalk's override accepted the kwarg. Streaming on Telegram/Discord/Slack/Matrix/Mattermost/Feishu/ WhatsApp raised TypeError the first time a segment break or final edit fired. The REQUIRES_EDIT_FINALIZE capability flag only gates the redundant final edit (and the identical-text short-circuit), not the kwarg itself — so adapters that opt out of finalize still receive the keyword argument and must accept it. Add *, finalize: bool = False to the 7 non-DingTalk signatures; the body ignores the arg since those platforms treat edits as stateless (consistent with the base class contract in base.py). Add a parametrized signature check over every concrete adapter class so a future override cannot silently drop the kwarg — existing tests use MagicMock which swallows any kwarg and cannot catch this. Fixes #12579 --- gateway/platforms/discord.py | 2 ++ gateway/platforms/feishu.py | 2 ++ gateway/platforms/matrix.py | 2 +- gateway/platforms/mattermost.py | 2 +- gateway/platforms/slack.py | 2 ++ gateway/platforms/telegram.py | 2 ++ gateway/platforms/whatsapp.py | 2 ++ tests/gateway/test_stream_consumer.py | 37 +++++++++++++++++++++++++++ 8 files changed, 49 insertions(+), 2 deletions(-) diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index 28286d48c..660ed46dd 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -1081,6 +1081,8 @@ class DiscordAdapter(BasePlatformAdapter): chat_id: str, message_id: str, content: str, + *, + finalize: bool = False, ) -> SendResult: """Edit a previously sent Discord message.""" if not self._client: diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 0531bff48..4b4fa0da4 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -1468,6 +1468,8 @@ class FeishuAdapter(BasePlatformAdapter): chat_id: str, message_id: str, content: str, + *, + finalize: bool = False, ) -> SendResult: """Edit a previously sent Feishu text/post message.""" if not self._client: diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index cdd67b337..a5f9352b5 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -825,7 +825,7 @@ class MatrixAdapter(BasePlatformAdapter): async def edit_message( - self, chat_id: str, message_id: str, content: str + self, chat_id: str, message_id: str, content: str, *, finalize: bool = False ) -> SendResult: """Edit an existing message (via m.replace).""" diff --git a/gateway/platforms/mattermost.py b/gateway/platforms/mattermost.py index 18367a8e4..10539bf64 100644 --- a/gateway/platforms/mattermost.py +++ b/gateway/platforms/mattermost.py @@ -304,7 +304,7 @@ class MattermostAdapter(BasePlatformAdapter): ) async def edit_message( - self, chat_id: str, message_id: str, content: str + self, chat_id: str, message_id: str, content: str, *, finalize: bool = False ) -> SendResult: """Edit an existing post.""" formatted = self.format_message(content) diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index ba444c53e..5455c0fa5 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -316,6 +316,8 @@ class SlackAdapter(BasePlatformAdapter): chat_id: str, message_id: str, content: str, + *, + finalize: bool = False, ) -> SendResult: """Edit a previously sent Slack message.""" if not self._app: diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index 0b74c4e15..1bc4ec2b1 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -1081,6 +1081,8 @@ class TelegramAdapter(BasePlatformAdapter): chat_id: str, message_id: str, content: str, + *, + finalize: bool = False, ) -> SendResult: """Edit a previously sent Telegram message.""" if not self._bot: diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index d1de5b856..78b1b92f7 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -655,6 +655,8 @@ class WhatsAppAdapter(BasePlatformAdapter): chat_id: str, message_id: str, content: str, + *, + finalize: bool = False, ) -> SendResult: """Edit a previously sent message via the WhatsApp bridge.""" if not self._running or not self._http_session: diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py index 0a0e0631d..7ae587dad 100644 --- a/tests/gateway/test_stream_consumer.py +++ b/tests/gateway/test_stream_consumer.py @@ -133,6 +133,43 @@ class TestFinalizeCapabilityGate: assert picky.edit_message.call_args[1]["finalize"] is True +class TestEditMessageFinalizeSignature: + """Every concrete platform adapter must accept the ``finalize`` kwarg. + + stream_consumer._send_or_edit always passes ``finalize=`` to + ``adapter.edit_message(...)`` (see gateway/stream_consumer.py). An + adapter that overrides edit_message without accepting finalize raises + TypeError the first time streaming hits a segment break or final edit. + Guard the contract with an explicit signature check so it cannot + silently regress — existing tests use MagicMock which swallows any + kwarg and cannot catch this. + """ + + @pytest.mark.parametrize( + "module_path,class_name", + [ + ("gateway.platforms.telegram", "TelegramAdapter"), + ("gateway.platforms.discord", "DiscordAdapter"), + ("gateway.platforms.slack", "SlackAdapter"), + ("gateway.platforms.matrix", "MatrixAdapter"), + ("gateway.platforms.mattermost", "MattermostAdapter"), + ("gateway.platforms.feishu", "FeishuAdapter"), + ("gateway.platforms.whatsapp", "WhatsAppAdapter"), + ("gateway.platforms.dingtalk", "DingTalkAdapter"), + ], + ) + def test_edit_message_accepts_finalize(self, module_path, class_name): + import inspect + + module = pytest.importorskip(module_path) + cls = getattr(module, class_name) + params = inspect.signature(cls.edit_message).parameters + assert "finalize" in params, ( + f"{class_name}.edit_message must accept 'finalize' kwarg; " + f"stream_consumer._send_or_edit passes it unconditionally" + ) + + class TestSendOrEditMediaStripping: """Verify _send_or_edit strips MEDIA: before sending to the platform."""