test(feishu): type tool harness fixtures

This commit is contained in:
Ayaan Zaidi 2026-03-27 11:53:58 +05:30
parent 1042710e3b
commit 6ad50ce474
No known key found for this signature in database
4 changed files with 111 additions and 94 deletions

View File

@ -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();
});
});

View File

@ -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<string, unknown> {
return typeof value === "object" && value !== null;
}
function readCallOptions(
mock: { mock: { calls: unknown[][] } },
index = -1,
): Record<string, unknown> {
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<unknown> } }
| 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<unknown> } }
| 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<unknown> } }
| 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<unknown> } }
| 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);

View File

@ -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<DocxDescendantCreate>[0];
type DocxDescendantCreateResponse = Awaited<ReturnType<DocxDescendantCreate>>;
function createDocxDescendantClient(
create: (params: DocxDescendantCreateParams) => Promise<DocxDescendantCreateResponse>,
): Pick<Lark.Client, "docx"> {
return {
docx: {
documentBlockDescendant: {
create,
},
},
};
}
function createCountingIterable<T>(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"]);

View File

@ -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<string, unknown> = {}) {
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<string, unknown>) => Promise<any> };
return tool as ToolLike;
}
it("inserts blocks sequentially to preserve document order", async () => {