From 6396faa2c38f3e2955f86f3633ca14438abd326c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 16:56:55 +0100 Subject: [PATCH] test(feishu): assert typing cleanup on dispatch paths (#27640) --- CHANGELOG.md | 1 + extensions/feishu/src/bot.test.ts | 86 +++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc34e8ec9d2..5ed1ac5c743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai - Feishu/Doc tools: route `feishu_doc` and `feishu_app_scopes` through the active agent account context (with explicit `accountId` override support) so multi-account agents no longer default to the first configured app, with regression coverage for context routing and explicit override behavior. (#27338) thanks @AaronL725. - Feishu/Inbound message metadata: include inbound `message_id` in `BodyForAgent` on a dedicated metadata line so agents can reliably correlate and act on media/message operations that require message IDs, with regression coverage. (#27253) thanks @xss925175263. - Feishu/Permission error dispatch: merge sender-name permission notices into the main inbound dispatch so one user message produces one agent turn/reply (instead of a duplicate permission-notice turn), with regression coverage. (#27381) thanks @byungsker. +- Feishu/Typing cleanup: always call dispatcher completion/idle cleanup in `finally` after Feishu dispatch, so typing indicator keepalive loops stop on both success and error paths; added regression coverage for cleanup on both paths. (#27640) thanks @kevinWangSheng. - Telegram/Inline buttons: allow callback-query button handling in groups (including `/models` follow-up buttons) when group policy authorizes the sender, by removing the redundant callback allowlist gate that blocked open-policy groups. (#27343) Thanks @GodsBoy. - Telegram/Streaming preview: when finalizing without an existing preview message, prime pending preview text with final answer before stop-flush so users do not briefly see stale 1-2 word fragments (for example `no` before `no problem`). (#27449) Thanks @emanuelst for the original fix direction in #19673. - Telegram/sendChatAction 401 handling: add bounded exponential backoff + temporary local typing suppression after repeated unauthorized failures to stop unbounded `sendChatAction` retry loops that can trigger Telegram abuse enforcement and bot deletion. (#27415) Thanks @widingmarcus-cyber. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 7e56c36c411..c4355215081 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -11,11 +11,14 @@ const { mockDownloadMessageResourceFeishu, mockCreateFeishuClient, } = vi.hoisted(() => ({ - mockCreateFeishuReplyDispatcher: vi.fn(() => ({ - dispatcher: vi.fn(), - replyOptions: {}, - markDispatchIdle: vi.fn(), - })), + mockCreateFeishuReplyDispatcher: vi.fn(() => { + const markComplete = vi.fn(); + return { + dispatcher: { markComplete }, + replyOptions: {}, + markDispatchIdle: vi.fn(), + }; + }), mockSendMessageFeishu: vi.fn().mockResolvedValue({ messageId: "pairing-msg", chatId: "oc-dm" }), mockGetMessageFeishu: vi.fn().mockResolvedValue(null), mockDownloadMessageResourceFeishu: vi.fn().mockResolvedValue({ @@ -517,4 +520,77 @@ describe("handleFeishuMessage command authorization", () => { }), ); }); + + it("always marks dispatcher complete and dispatch idle after successful dispatch", async () => { + mockShouldComputeCommandAuthorized.mockReturnValue(false); + const cfg: ClawdbotConfig = { + channels: { + feishu: { + dmPolicy: "open", + }, + }, + } as ClawdbotConfig; + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-success", + }, + }, + message: { + message_id: "msg-success-cleanup", + chat_id: "oc-dm", + chat_type: "p2p", + message_type: "text", + content: JSON.stringify({ text: "hello" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + const dispatcherResult = mockCreateFeishuReplyDispatcher.mock.results.at(-1)?.value as + | { + dispatcher: { markComplete: ReturnType }; + markDispatchIdle: ReturnType; + } + | undefined; + expect(dispatcherResult?.dispatcher.markComplete).toHaveBeenCalledTimes(1); + expect(dispatcherResult?.markDispatchIdle).toHaveBeenCalledTimes(1); + }); + + it("marks dispatcher complete and dispatch idle when dispatch throws", async () => { + mockShouldComputeCommandAuthorized.mockReturnValue(false); + mockDispatchReplyFromConfig.mockRejectedValueOnce(new Error("dispatch boom")); + const cfg: ClawdbotConfig = { + channels: { + feishu: { + dmPolicy: "open", + }, + }, + } as ClawdbotConfig; + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-fail", + }, + }, + message: { + message_id: "msg-error-cleanup", + chat_id: "oc-dm", + chat_type: "p2p", + message_type: "text", + content: JSON.stringify({ text: "hello" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + const dispatcherResult = mockCreateFeishuReplyDispatcher.mock.results.at(-1)?.value as + | { + dispatcher: { markComplete: ReturnType }; + markDispatchIdle: ReturnType; + } + | undefined; + expect(dispatcherResult?.dispatcher.markComplete).toHaveBeenCalledTimes(1); + expect(dispatcherResult?.markDispatchIdle).toHaveBeenCalledTimes(1); + }); });