Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion plugins/platforms/feishu/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2859,6 +2859,7 @@ async def _handle_card_action_event(self, data: Any) -> None:

context = getattr(event, "context", None)
chat_id = str(getattr(context, "open_chat_id", "") or "")
open_message_id = str(getattr(context, "open_message_id", "") or "")
operator = getattr(event, "operator", None)
open_id = str(getattr(operator, "open_id", "") or "")
if not chat_id or not open_id:
Expand Down Expand Up @@ -2893,7 +2894,7 @@ async def _handle_card_action_event(self, data: Any) -> None:
message_type=MessageType.COMMAND,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add focused coverage for this path: provide context.open_message_id in the card-action fixture and assert that the dispatched synthetic event uses it as message_id. The existing non-approval card-action test only checks the generated text.

source=source,
raw_message=data,
message_id=token or str(uuid.uuid4()),
message_id=open_message_id or token or str(uuid.uuid4()),
timestamp=datetime.now(),
)
logger.info("[Feishu] Routing card action %r from %s in %s as synthetic command", action_tag, open_id, chat_id)
Expand Down
56 changes: 55 additions & 1 deletion tests/gateway/test_feishu_approval_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ def _make_card_action_data(
chat_id: str = "oc_12345",
open_id: str = "ou_user1",
token: str = "tok_abc",
open_message_id: str = "",
) -> SimpleNamespace:
"""Create a mock Feishu card action callback data object."""
return SimpleNamespace(
event=SimpleNamespace(
token=token,
context=SimpleNamespace(open_chat_id=chat_id),
context=SimpleNamespace(open_chat_id=chat_id, open_message_id=open_message_id),
operator=SimpleNamespace(open_id=open_id),
action=SimpleNamespace(
tag="button",
Expand Down Expand Up @@ -437,6 +438,59 @@ async def test_routes_as_synthetic_command(self):
event = mock_handle.call_args[0][0]
assert "/card button" in event.text

@pytest.mark.asyncio
async def test_uses_open_message_id_when_available(self):
"""When open_message_id is present in the callback context, it should
be used as the synthetic event's message_id so downstream reply
anchors resolve to a valid Feishu message (om_xxx), not the card
action token (c-xxx)."""
adapter = _make_adapter()

data = _make_card_action_data(
action_value={"custom_action": "recognize"},
token="c-deadbeef",
open_message_id="om_card_message_123",
)

with (
patch.object(
adapter, "_resolve_sender_profile", new_callable=AsyncMock,
return_value={"user_id": "ou_u", "user_name": "Dave", "user_id_alt": None},
),
patch.object(adapter, "get_chat_info", new_callable=AsyncMock, return_value={"name": "Test Chat"}),
patch.object(adapter, "_handle_message_with_guards", new_callable=AsyncMock) as mock_handle,
):
await adapter._handle_card_action_event(data)

event = mock_handle.call_args[0][0]
assert event.message_id == "om_card_message_123"

@pytest.mark.asyncio
async def test_falls_back_to_token_when_open_message_id_absent(self):
"""When open_message_id is absent from the callback context (e.g.
older Feishu SDK or edge-case payload), message_id should fall back
to the card action token for deduplication purposes."""
adapter = _make_adapter()

data = _make_card_action_data(
action_value={"custom_action": "recognize"},
token="c-fallback-token",
open_message_id="", # absent
)

with (
patch.object(
adapter, "_resolve_sender_profile", new_callable=AsyncMock,
return_value={"user_id": "ou_u", "user_name": "Dave", "user_id_alt": None},
),
patch.object(adapter, "get_chat_info", new_callable=AsyncMock, return_value={"name": "Test Chat"}),
patch.object(adapter, "_handle_message_with_guards", new_callable=AsyncMock) as mock_handle,
):
await adapter._handle_card_action_event(data)

event = mock_handle.call_args[0][0]
assert event.message_id == "c-fallback-token"


# ===========================================================================
# _on_card_action_trigger β€” inline card response for approval actions
Expand Down