import { mkdir, mkdtemp, readFile, realpath, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Readable } from "node:stream"; import { describe, expect, it, vi } from "vitest"; import { claudeSdkToolConfigForRole, cphHubMcpToolsForRole } from "../../src/agent/roleTools.js"; import type { FeishuRuntime } from "../../src/feishu/client.js"; import type { FeishuBotCli } from "../../src/feishu/botCli.js"; import { writeNewWorkspaceFileNoFollow } from "../../src/security/workspaceFiles.js"; import { downloadFeishuMessageResource } from "../../src/feishu/download.js"; const itOnLinux = process.platform === "linux" ? it : it.skip; describe("Feishu message resource download", () => { it("exposes the download tool to default and explicitly configured roles", () => { expect(cphHubMcpToolsForRole(undefined)).toContain("feishu_download_resource"); expect(cphHubMcpToolsForRole(["feishu_download_resource"])).toEqual([ "todo_write", "feishu_download_resource", ]); expect(claudeSdkToolConfigForRole(["feishu_download_resource"]).allowedTools).toEqual([ "mcp__cph_hub__feishu_download_resource", ]); }); itOnLinux("downloads a bound-chat image into the project inbox", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "hub-feishu-download-")); const workspaceDir = join(workspaceRoot, "project"); await mkdir(workspaceDir); try { const messageGet = vi.fn(async () => ({ data: { items: [{ chat_id: "chat-1" }] } })); const messageResourceGet = vi.fn(); const rt = mockRuntime(messageGet, messageResourceGet); const botCli = fakeBotCli(); const result = await downloadFeishuMessageResource( { messageId: "message-1", fileKey: "img-key-1", resourceType: "image" }, { boundChatId: "chat-1", workspaceRoot, workspaceDir, botCli }, rt, ); expect(result).toMatchObject({ resourceType: "image" }); expect(result.path).toMatch( new RegExp(`^${escapeRegExp(join(await realpath(workspaceDir), ".cph", "inbox"))}`), ); expect(result.path).toMatch(/\.png$/); await expect(readFile(result.path, "utf8")).resolves.toBe("image bytes"); expect(messageResourceGet).not.toHaveBeenCalled(); } finally { await rm(workspaceRoot, { recursive: true, force: true }); } }); it("rejects resources from a message outside the bound chat", async () => { const messageGet = vi.fn(async () => ({ data: { items: [{ chat_id: "chat-other" }] } })); const messageResourceGet = vi.fn(); const rt = mockRuntime(messageGet, messageResourceGet); await expect(downloadFeishuMessageResource( { messageId: "message-other", fileKey: "img-key-other", resourceType: "image" }, { boundChatId: "chat-1", workspaceRoot: "/tmp", workspaceDir: "/tmp/project-1", botCli: fakeBotCli(), }, rt, )).rejects.toThrow("current project's bound chat"); }); }); function mockRuntime( messageGet: (payload: unknown) => Promise, messageResourceGet: (payload: unknown) => Promise, ): FeishuRuntime { return { client: { im: { v1: { message: { get: messageGet }, messageResource: { get: messageResourceGet }, }, }, } as unknown as FeishuRuntime["client"], logger: { info() {}, warn() {}, error() {}, debug() {}, fatal() {}, child() { return this; }, level: "silent", } as unknown as FeishuRuntime["logger"], }; } function fakeBotCli(): FeishuBotCli { return { downloadResource: (request) => writeNewWorkspaceFileNoFollow( request.workspaceRoot, request.workspaceDir, request.workspaceRelativePath, Readable.from([Buffer.from("image bytes")]), request.maxBytes, ), }; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }