fix(gateway): accept finalize kwarg in all platform edit_message overrides

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
This commit is contained in:
JackJin
2026-04-19 23:33:43 +08:00
committed by Teknium
parent fc5fda5e38
commit 6c0c625952
8 changed files with 49 additions and 2 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)."""

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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."""