From 8beb048a84cf9c400151bcc9cfef37333d1f7f55 Mon Sep 17 00:00:00 2001 From: YAXUAN Date: Sat, 28 Feb 2026 12:49:05 +0800 Subject: [PATCH] test(feishu): add regression for audio download resource type=file (openclaw#16311) thanks @Yaxuan42 Verified: - pnpm build - pnpm check - pnpm vitest run --config vitest.extensions.config.ts extensions/feishu/src/bot.test.ts extensions/feishu/src/media.test.ts Co-authored-by: Yaxuan42 <184813557+Yaxuan42@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> --- CHANGELOG.md | 1 + extensions/feishu/src/bot.test.ts | 18 ++++++++- extensions/feishu/src/bot.ts | 10 ++++- extensions/feishu/src/media.test.ts | 59 +++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 056189e6b..82003acd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai - Feishu/Local media sends: propagate `mediaLocalRoots` through Feishu outbound media sending into `loadWebMedia` so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth. - Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups..allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild. - Feishu/Docx append/write ordering: insert converted Docx blocks sequentially (single-block creates) so Feishu append/write preserves markdown block order instead of returning shuffled sections in asynchronous batch inserts. (#26172, #26022) Thanks @echoVic. +- Feishu/Inbound media regression coverage: add explicit tests for message resource type mapping (`image` stays `image`, non-image maps to `file`) to prevent reintroducing unsupported Feishu `type=audio` fetches. (#16311, #8746) Thanks @Yaxuan42. - Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3. - Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc. - Telegram/Reply media context: include replied media files in inbound context when replying to media, defer reply-media downloads to debounce flush, gate reply-media fetch behind DM authorization, and preserve replied media when non-vision sticker fallback runs (including cached-sticker paths). (#28488) Thanks @obviyus. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index b679bd71d..26164d4ec 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1,7 +1,7 @@ import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "openclaw/plugin-sdk"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { FeishuMessageEvent } from "./bot.js"; -import { buildFeishuAgentBody, handleFeishuMessage } from "./bot.js"; +import { buildFeishuAgentBody, handleFeishuMessage, toMessageResourceType } from "./bot.js"; import { setFeishuRuntime } from "./runtime.js"; const { @@ -993,3 +993,19 @@ describe("handleFeishuMessage command authorization", () => { ); }); }); + +describe("toMessageResourceType", () => { + it("maps image to image", () => { + expect(toMessageResourceType("image")).toBe("image"); + }); + + it("maps audio to file", () => { + expect(toMessageResourceType("audio")).toBe("file"); + }); + + it("maps video/file/sticker to file", () => { + expect(toMessageResourceType("video")).toBe("file"); + expect(toMessageResourceType("file")).toBe("file"); + expect(toMessageResourceType("sticker")).toBe("file"); + }); +}); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index 2486e6a96..b74d71e57 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -460,6 +460,14 @@ function parsePostContent(content: string): { } } +/** + * Map Feishu message type to messageResource.get resource type. + * Feishu messageResource API supports only: image | file. + */ +export function toMessageResourceType(messageType: string): "image" | "file" { + return messageType === "image" ? "image" : "file"; +} + /** * Infer placeholder text based on message type. */ @@ -570,7 +578,7 @@ async function resolveFeishuMediaList(params: { return []; } - const resourceType = messageType === "image" ? "image" : "file"; + const resourceType = toMessageResourceType(messageType); const result = await downloadMessageResourceFeishu({ cfg, messageId, diff --git a/extensions/feishu/src/media.test.ts b/extensions/feishu/src/media.test.ts index 8d1c61d3b..de6e73c43 100644 --- a/extensions/feishu/src/media.test.ts +++ b/extensions/feishu/src/media.test.ts @@ -335,3 +335,62 @@ describe("sendMediaFeishu msg_type routing", () => { expect(messageResourceGetMock).not.toHaveBeenCalled(); }); }); + +describe("downloadMessageResourceFeishu", () => { + beforeEach(() => { + vi.clearAllMocks(); + + resolveFeishuAccountMock.mockReturnValue({ + configured: true, + accountId: "main", + config: {}, + appId: "app_id", + appSecret: "app_secret", + domain: "feishu", + }); + + createFeishuClientMock.mockReturnValue({ + im: { + messageResource: { + get: messageResourceGetMock, + }, + }, + }); + + messageResourceGetMock.mockResolvedValue(Buffer.from("fake-audio-data")); + }); + + // Regression: Feishu API only supports type=image|file for messageResource.get. + // Audio/video resources must use type=file, not type=audio (#8746). + it("forwards provided type=file for non-image resources", async () => { + const result = await downloadMessageResourceFeishu({ + cfg: {} as any, + messageId: "om_audio_msg", + fileKey: "file_key_audio", + type: "file", + }); + + expect(messageResourceGetMock).toHaveBeenCalledWith({ + path: { message_id: "om_audio_msg", file_key: "file_key_audio" }, + params: { type: "file" }, + }); + expect(result.buffer).toBeInstanceOf(Buffer); + }); + + it("image uses type=image", async () => { + messageResourceGetMock.mockResolvedValue(Buffer.from("fake-image-data")); + + const result = await downloadMessageResourceFeishu({ + cfg: {} as any, + messageId: "om_img_msg", + fileKey: "img_key_1", + type: "image", + }); + + expect(messageResourceGetMock).toHaveBeenCalledWith({ + path: { message_id: "om_img_msg", file_key: "img_key_1" }, + params: { type: "image" }, + }); + expect(result.buffer).toBeInstanceOf(Buffer); + }); +});