diff --git a/extensions/feishu/src/chat.test.ts b/extensions/feishu/src/chat.test.ts index d06442b12f8..f154389edbd 100644 --- a/extensions/feishu/src/chat.test.ts +++ b/extensions/feishu/src/chat.test.ts @@ -1,4 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js"; +import type { OpenClawPluginApi } from "../runtime-api.js"; import { registerFeishuChatTools } from "./chat.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); @@ -11,6 +13,21 @@ vi.mock("./client.js", () => ({ })); describe("registerFeishuChatTools", () => { + function createChatToolApi(params: { + config: OpenClawPluginApi["config"]; + registerTool: OpenClawPluginApi["registerTool"]; + }): OpenClawPluginApi { + return createTestPluginApi({ + id: "feishu-test", + name: "Feishu Test", + source: "local", + config: params.config, + runtime: { log: vi.fn(), error: vi.fn() }, + logger: { debug: vi.fn(), info: vi.fn() }, + registerTool: params.registerTool, + }); + } + beforeEach(() => { vi.clearAllMocks(); createFeishuClientMock.mockReturnValue({ @@ -26,20 +43,21 @@ describe("registerFeishuChatTools", () => { it("registers feishu_chat and handles info/members actions", async () => { const registerTool = vi.fn(); - registerFeishuChatTools({ - config: { - channels: { - feishu: { - enabled: true, - appId: "app_id", - appSecret: "app_secret", // pragma: allowlist secret - tools: { chat: true }, + registerFeishuChatTools( + createChatToolApi({ + config: { + channels: { + feishu: { + enabled: true, + appId: "app_id", + appSecret: "app_secret", // pragma: allowlist secret + tools: { chat: true }, + }, }, }, - } as any, - logger: { debug: vi.fn(), info: vi.fn() } as any, - registerTool, - } as any); + registerTool, + }), + ); expect(registerTool).toHaveBeenCalledTimes(1); const tool = registerTool.mock.calls[0]?.[0]; @@ -98,20 +116,21 @@ describe("registerFeishuChatTools", () => { it("skips registration when chat tool is disabled", () => { const registerTool = vi.fn(); - registerFeishuChatTools({ - config: { - channels: { - feishu: { - enabled: true, - appId: "app_id", - appSecret: "app_secret", // pragma: allowlist secret - tools: { chat: false }, + registerFeishuChatTools( + createChatToolApi({ + config: { + channels: { + feishu: { + enabled: true, + appId: "app_id", + appSecret: "app_secret", // pragma: allowlist secret + tools: { chat: false }, + }, }, }, - } as any, - logger: { debug: vi.fn(), info: vi.fn() } as any, - registerTool, - } as any); + registerTool, + }), + ); expect(registerTool).not.toHaveBeenCalled(); }); }); diff --git a/extensions/feishu/src/client.test.ts b/extensions/feishu/src/client.test.ts index 218893ffca8..68ea308f14d 100644 --- a/extensions/feishu/src/client.test.ts +++ b/extensions/feishu/src/client.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js"; import type { OpenClawPluginApi } from "../runtime-api.js"; import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js"; @@ -99,12 +100,24 @@ const baseAccount: ResolvedFeishuAccount = { appId: "app_123", appSecret: "secret_123", // pragma: allowlist secret domain: "feishu", - config: {} as FeishuConfig, + config: {}, }; +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function readCallOptions( + mock: { mock: { calls: unknown[][] } }, + index = -1, +): Record { + const call = index < 0 ? mock.mock.calls.at(index)?.[0] : mock.mock.calls[index]?.[0]; + return isRecord(call) ? call : {}; +} + function firstWsClientOptions(): { agent?: unknown } { - const calls = wsClientCtorMock.mock.calls as unknown as Array<[options: { agent?: unknown }]>; - return calls[0]?.[0] ?? {}; + const options = readCallOptions(wsClientCtorMock, 0); + return { agent: options.agent }; } beforeAll(async () => { @@ -179,11 +192,8 @@ afterEach(() => { describe("createFeishuClient HTTP timeout", () => { const getLastClientHttpInstance = () => { - const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>; - const lastCall = calls[calls.length - 1]?.[0] as - | { httpInstance?: { get: (...args: unknown[]) => Promise } } - | undefined; - return lastCall?.httpInstance; + const httpInstance = readCallOptions(clientCtorMock).httpInstance; + return isRecord(httpInstance) ? httpInstance : undefined; }; const expectGetCallTimeout = async (timeout: number) => { @@ -199,22 +209,16 @@ describe("createFeishuClient HTTP timeout", () => { it("passes a custom httpInstance with default timeout to Lark.Client", () => { createFeishuClient({ appId: "app_1", appSecret: "secret_1", accountId: "timeout-test" }); // pragma: allowlist secret - const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>; - const lastCall = calls[calls.length - 1]?.[0] as { httpInstance?: unknown } | undefined; - expect(lastCall?.httpInstance).toBeDefined(); + expect(readCallOptions(clientCtorMock).httpInstance).toBeDefined(); }); it("injects default timeout into HTTP request options", async () => { createFeishuClient({ appId: "app_2", appSecret: "secret_2", accountId: "timeout-inject" }); // pragma: allowlist secret - const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>; - const lastCall = calls[calls.length - 1]?.[0] as - | { httpInstance: { post: (...args: unknown[]) => Promise } } - | undefined; - const httpInstance = lastCall?.httpInstance; + const httpInstance = getLastClientHttpInstance(); expect(httpInstance).toBeDefined(); - await httpInstance?.post( + await httpInstance?.post?.( "https://example.com/api", { data: 1 }, { headers: { "X-Custom": "yes" } }, @@ -230,14 +234,10 @@ describe("createFeishuClient HTTP timeout", () => { it("allows explicit timeout override per-request", async () => { createFeishuClient({ appId: "app_3", appSecret: "secret_3", accountId: "timeout-override" }); // pragma: allowlist secret - const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>; - const lastCall = calls[calls.length - 1]?.[0] as - | { httpInstance: { get: (...args: unknown[]) => Promise } } - | undefined; - const httpInstance = lastCall?.httpInstance; + const httpInstance = getLastClientHttpInstance(); expect(httpInstance).toBeDefined(); - await httpInstance?.get("https://example.com/api", { timeout: 5_000 }); + await httpInstance?.get?.("https://example.com/api", { timeout: 5_000 }); expect(mockBaseHttpInstance.get).toHaveBeenCalledWith( "https://example.com/api", @@ -320,14 +320,10 @@ describe("createFeishuClient HTTP timeout", () => { config: { httpTimeoutMs: 45_000 }, }); - const calls = clientCtorMock.mock.calls as unknown as Array<[options: unknown]>; - expect(calls.length).toBe(2); - - const lastCall = calls[calls.length - 1]?.[0] as - | { httpInstance: { get: (...args: unknown[]) => Promise } } - | undefined; - expect(lastCall?.httpInstance).toBeDefined(); - await lastCall?.httpInstance.get("https://example.com/api"); + expect(clientCtorMock.mock.calls.length).toBe(2); + const httpInstance = getLastClientHttpInstance(); + expect(httpInstance).toBeDefined(); + await httpInstance?.get?.("https://example.com/api"); expect(mockBaseHttpInstance.get).toHaveBeenCalledWith( "https://example.com/api", @@ -340,13 +336,15 @@ describe("feishu plugin register", () => { it("registers the Feishu channel, tools, and subagent hooks", async () => { const { default: plugin } = await import("../index.js"); const registerChannel = vi.fn(); - const api = { + const api = createTestPluginApi({ + id: "feishu-test", + name: "Feishu Test", + source: "local", runtime: { log: vi.fn() }, - registerChannel, on: vi.fn(), config: {}, - registrationMode: "full", - } as unknown as OpenClawPluginApi; + registerChannel, + }); plugin.register(api); diff --git a/extensions/feishu/src/docx-batch-insert.test.ts b/extensions/feishu/src/docx-batch-insert.test.ts index 239e46738b4..79458b95896 100644 --- a/extensions/feishu/src/docx-batch-insert.test.ts +++ b/extensions/feishu/src/docx-batch-insert.test.ts @@ -1,5 +1,23 @@ +import type * as Lark from "@larksuiteoapi/node-sdk"; import { describe, expect, it, vi } from "vitest"; import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js"; +import type { FeishuDocxBlock } from "./docx-types.js"; + +type DocxDescendantCreate = Lark.Client["docx"]["documentBlockDescendant"]["create"]; +type DocxDescendantCreateParams = Parameters[0]; +type DocxDescendantCreateResponse = Awaited>; + +function createDocxDescendantClient( + create: (params: DocxDescendantCreateParams) => Promise, +): Pick { + return { + docx: { + documentBlockDescendant: { + create, + }, + }, + }; +} function createCountingIterable(values: T[]) { let iterations = 0; @@ -28,18 +46,12 @@ describe("insertBlocksInBatches", () => { children: data.children_id.map((id) => ({ block_id: id })), }, })); - const client = { - docx: { - documentBlockDescendant: { - create: createMock, - }, - }, - } as any; + const client = createDocxDescendantClient(createMock); const result = await insertBlocksInBatches( client, "doc_1", - counting.values as any[], + Array.from(counting.values), blocks.map((block) => block.block_id), ); @@ -63,21 +75,15 @@ describe("insertBlocksInBatches", () => { }, }), ); - const client = { - docx: { - documentBlockDescendant: { - create: createMock, - }, - }, - } as any; - const blocks = [ + const client = createDocxDescendantClient(createMock); + const blocks: FeishuDocxBlock[] = [ { block_id: "root_a", block_type: 1, children: ["child_a"] }, { block_id: "child_a", block_type: 2 }, { block_id: "root_b", block_type: 1, children: ["child_b"] }, { block_id: "child_b", block_type: 2 }, ]; - await insertBlocksInBatches(client, "doc_1", blocks as any[], ["root_a", "root_b"]); + await insertBlocksInBatches(client, "doc_1", blocks, ["root_a", "root_b"]); expect(createMock).toHaveBeenCalledTimes(1); expect(createMock.mock.calls[0]?.[0]?.data.children_id).toEqual(["root_a", "root_b"]); diff --git a/extensions/feishu/src/docx.test.ts b/extensions/feishu/src/docx.test.ts index 31779901386..aec9f56260c 100644 --- a/extensions/feishu/src/docx.test.ts +++ b/extensions/feishu/src/docx.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createToolFactoryHarness, type ToolLike } from "./tool-factory-test-harness.js"; const createFeishuClientMock = vi.hoisted(() => vi.fn()); const fetchRemoteMediaMock = vi.hoisted(() => vi.fn()); @@ -115,26 +116,19 @@ describe("feishu_doc image fetch hardening", () => { }); function resolveFeishuDocTool(context: Record = {}) { - const registerTool = vi.fn(); - registerFeishuDocTools({ - config: { - channels: { - feishu: { - appId: "app_id", - appSecret: "app_secret", - }, + const harness = createToolFactoryHarness({ + channels: { + feishu: { + enabled: true, + appId: "app_id", + appSecret: "app_secret", }, - } as any, - logger: { debug: vi.fn(), info: vi.fn() } as any, - registerTool, - } as any); - - const tool = registerTool.mock.calls - .map((call) => call[0]) - .map((candidate) => (typeof candidate === "function" ? candidate(context) : candidate)) - .find((candidate) => candidate.name === "feishu_doc"); + }, + }); + registerFeishuDocTools(harness.api); + const tool = harness.resolveTool("feishu_doc", context); expect(tool).toBeDefined(); - return tool as { execute: (callId: string, params: Record) => Promise }; + return tool as ToolLike; } it("inserts blocks sequentially to preserve document order", async () => {