diff --git a/extensions/feishu/src/bot.broadcast.test.ts b/extensions/feishu/src/bot.broadcast.test.ts index 8f718ad8e21..1d6aa878cd7 100644 --- a/extensions/feishu/src/bot.broadcast.test.ts +++ b/extensions/feishu/src/bot.broadcast.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { EnvelopeFormatOptions } from "../../../src/auto-reply/envelope.js"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, PluginRuntime } from "../runtime-api.js"; @@ -15,6 +16,7 @@ const { mockCreateFeishuReplyDispatcher, mockCreateFeishuClient, mockResolveAgen sendFinalReply: vi.fn(), waitForIdle: vi.fn(), getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })), + getFailedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })), markComplete: vi.fn(), }, replyOptions: {}, @@ -33,28 +35,37 @@ vi.mock("./client.js", () => ({ })); describe("broadcast dispatch", () => { - const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx); + const finalizeInboundContextCalls: Array> = []; + const mockFinalizeInboundContext: PluginRuntime["channel"]["reply"]["finalizeInboundContext"] = ( + ctx, + ) => { + finalizeInboundContextCalls.push(ctx); + return { + ...ctx, + CommandAuthorized: typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : false, + }; + }; const mockDispatchReplyFromConfig = vi .fn() .mockResolvedValue({ queuedFinal: false, counts: { final: 1 } }); - const mockWithReplyDispatcher = vi.fn( - async ({ - dispatcher, - run, - onSettled, - }: Parameters[0]) => { + const mockWithReplyDispatcher: PluginRuntime["channel"]["reply"]["withReplyDispatcher"] = async ({ + dispatcher, + run, + onSettled, + }) => { + try { + return await run(); + } finally { + dispatcher.markComplete(); try { - return await run(); + await dispatcher.waitForIdle(); } finally { - dispatcher.markComplete(); - try { - await dispatcher.waitForIdle(); - } finally { - await onSettled?.(); - } + await onSettled?.(); } - }, - ); + } + }; + const resolveEnvelopeFormatOptionsMock: PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"] = + () => ({}) satisfies EnvelopeFormatOptions; const mockShouldComputeCommandAuthorized = vi.fn(() => false); const mockSaveMediaBuffer = vi.fn().mockResolvedValue({ path: "/tmp/inbound-clip.mp4", @@ -108,12 +119,14 @@ describe("broadcast dispatch", () => { beforeEach(() => { vi.clearAllMocks(); + finalizeInboundContextCalls.length = 0; mockResolveAgentRoute.mockReturnValue({ agentId: "main", channel: "feishu", accountId: "default", sessionKey: "agent:main:feishu:group:oc-broadcast-group", mainSessionKey: "agent:main:main", + lastRoutePolicy: "session", matchedBy: "default", }); mockCreateFeishuClient.mockReturnValue({ @@ -133,7 +146,7 @@ describe("broadcast dispatch", () => { resolveAgentRoute: (params) => mockResolveAgentRoute(params), }, reply: { - resolveEnvelopeFormatOptions: vi.fn(() => ({ template: "channel+name+time" })), + resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock, formatAgentEnvelope: vi.fn((params: { body: string }) => params.body), finalizeInboundContext: mockFinalizeInboundContext, dispatchReplyFromConfig: mockDispatchReplyFromConfig, @@ -175,9 +188,7 @@ describe("broadcast dispatch", () => { }); expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2); - const sessionKeys = mockFinalizeInboundContext.mock.calls.map( - (call: unknown[]) => (call[0] as { SessionKey: string }).SessionKey, - ); + const sessionKeys = finalizeInboundContextCalls.map((call) => call.SessionKey); expect(sessionKeys).toContain("agent:susan:feishu:group:oc-broadcast-group"); expect(sessionKeys).toContain("agent:main:feishu:group:oc-broadcast-group"); expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1); @@ -253,7 +264,7 @@ describe("broadcast dispatch", () => { expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledTimes(1); - expect(mockFinalizeInboundContext).toHaveBeenCalledWith( + expect(finalizeInboundContextCalls).toContainEqual( expect.objectContaining({ SessionKey: "agent:main:feishu:group:oc-broadcast-group", }), @@ -295,7 +306,7 @@ describe("broadcast dispatch", () => { expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(2); mockDispatchReplyFromConfig.mockClear(); - mockFinalizeInboundContext.mockClear(); + finalizeInboundContextCalls.length = 0; await handleFeishuMessage({ cfg, @@ -339,8 +350,7 @@ describe("broadcast dispatch", () => { }); expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); - const sessionKey = (mockFinalizeInboundContext.mock.calls[0]?.[0] as { SessionKey: string }) - .SessionKey; + const sessionKey = String(finalizeInboundContextCalls[0]?.SessionKey ?? ""); expect(sessionKey).toBe("agent:susan:feishu:group:oc-broadcast-group"); }); }); diff --git a/extensions/feishu/src/bot.card-action.test.ts b/extensions/feishu/src/bot.card-action.test.ts index daf00c19294..9613aefacaa 100644 --- a/extensions/feishu/src/bot.card-action.test.ts +++ b/extensions/feishu/src/bot.card-action.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { handleFeishuCardAction, @@ -35,7 +36,7 @@ import { handleFeishuMessage } from "./bot.js"; describe("Feishu Card Action Handler", () => { const cfg: ClawdbotConfig = {}; - const runtime: RuntimeEnv = { log: vi.fn(), error: vi.fn() }; + const runtime: RuntimeEnv = createRuntimeEnv(); function createCardActionEvent(params: { token: string; diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index 97a76c0f7a1..259da551f99 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -1,5 +1,6 @@ import type * as ConversationRuntime from "openclaw/plugin-sdk/conversation-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ResolvedAgentRoute } from "../../../src/routing/resolve-route.js"; import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, PluginRuntime, RuntimeEnv } from "../runtime-api.js"; @@ -31,6 +32,7 @@ function createReplyDispatcher(): ReplyDispatcher { sendFinalReply: vi.fn(), waitForIdle: vi.fn(), getQueuedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })), + getFailedCounts: vi.fn(() => ({ tool: 0, block: 0, final: 0 })), markComplete: vi.fn(), }; } @@ -38,42 +40,59 @@ function createReplyDispatcher(): ReplyDispatcher { function createConfiguredFeishuRoute(): NonNullable { return { bindingResolution: { - configuredBinding: { - spec: { - channel: "feishu", - accountId: "default", - conversationId: "ou_sender_1", - agentId: "codex", - mode: "persistent", - }, - record: { - bindingId: "config:acp:feishu:default:ou_sender_1", - targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123", - targetKind: "session", - conversation: { - channel: "feishu", - accountId: "default", - conversationId: "ou_sender_1", - }, - status: "active", - boundAt: 0, - metadata: { source: "config" }, - }, - }, - statefulTarget: { - kind: "stateful", - driverId: "acp", - sessionKey: "agent:codex:acp:binding:feishu:default:abc123", - agentId: "codex", - }, - }, - configuredBinding: { - spec: { + conversation: { channel: "feishu", accountId: "default", conversationId: "ou_sender_1", + }, + compiledBinding: { + channel: "feishu", + accountPattern: "default", + binding: { + type: "acp", + agentId: "codex", + match: { + channel: "feishu", + accountId: "default", + peer: { kind: "direct", id: "ou_sender_1" }, + }, + }, + bindingConversationId: "ou_sender_1", + target: { + conversationId: "ou_sender_1", + }, agentId: "codex", - mode: "persistent", + provider: { + compileConfiguredBinding: () => ({ conversationId: "ou_sender_1" }), + matchInboundConversation: () => ({ conversationId: "ou_sender_1" }), + }, + targetFactory: { + driverId: "acp", + materialize: () => ({ + record: { + bindingId: "config:acp:feishu:default:ou_sender_1", + targetSessionKey: "agent:codex:acp:binding:feishu:default:abc123", + targetKind: "session", + conversation: { + channel: "feishu", + accountId: "default", + conversationId: "ou_sender_1", + }, + status: "active", + boundAt: 0, + metadata: { source: "config" }, + }, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: "agent:codex:acp:binding:feishu:default:abc123", + agentId: "codex", + }, + }), + }, + }, + match: { + conversationId: "ou_sender_1", }, record: { bindingId: "config:acp:feishu:default:ou_sender_1", @@ -88,6 +107,12 @@ function createConfiguredFeishuRoute(): NonNullable { boundAt: 0, metadata: { source: "config" }, }, + statefulTarget: { + kind: "stateful", + driverId: "acp", + sessionKey: "agent:codex:acp:binding:feishu:default:abc123", + agentId: "codex", + }, }, route: { agentId: "codex", @@ -95,6 +120,7 @@ function createConfiguredFeishuRoute(): NonNullable { accountId: "default", sessionKey: "agent:codex:acp:binding:feishu:default:abc123", mainSessionKey: "agent:codex:main", + lastRoutePolicy: "session", matchedBy: "binding.channel", }, }; @@ -120,13 +146,14 @@ function createBoundConversation(): NonNullable { }; } -function buildDefaultResolveRoute() { +function buildDefaultResolveRoute(): ResolvedAgentRoute { return { agentId: "main", channel: "feishu", accountId: "default", sessionKey: "agent:main:feishu:dm:ou-attacker", mainSessionKey: "agent:main:main", + lastRoutePolicy: "session", matchedBy: "default", }; } @@ -138,14 +165,11 @@ const readSessionUpdatedAtMock: PluginRuntime["channel"]["session"]["readSession ) => mockReadSessionUpdatedAt(params); const resolveStorePathMock: PluginRuntime["channel"]["session"]["resolveStorePath"] = (params) => mockResolveStorePath(params); -const resolveEnvelopeFormatOptionsMock: PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"] = - () => ({}); -const finalizeInboundContextMock: PluginRuntime["channel"]["reply"]["finalizeInboundContext"] = ( - ctx, -) => ctx; -const withReplyDispatcherMock: PluginRuntime["channel"]["reply"]["withReplyDispatcher"] = async ({ +const resolveEnvelopeFormatOptionsMock = () => ({}); +const finalizeInboundContextMock = (ctx: Record) => ctx; +const withReplyDispatcherMock = async ({ run, -}) => await run(); +}: Parameters[0]) => await run(); const { mockCreateFeishuReplyDispatcher, @@ -176,17 +200,22 @@ const { fileName: "clip.mp4", }), mockCreateFeishuClient: vi.fn(), - mockResolveAgentRoute: vi.fn(buildDefaultResolveRoute), - mockReadSessionUpdatedAt: vi.fn(), - mockResolveStorePath: vi.fn(() => "/tmp/feishu-sessions.json"), + mockResolveAgentRoute: vi.fn((_params?: unknown) => buildDefaultResolveRoute()), + mockReadSessionUpdatedAt: vi.fn((_params?: unknown): number | undefined => undefined), + mockResolveStorePath: vi.fn((_params?: unknown) => "/tmp/feishu-sessions.json"), mockResolveConfiguredBindingRoute: vi.fn( - ({ route }: { route: NonNullable["route"] }) => ({ + ({ + route, + }: { + route: NonNullable["route"]; + }): ConfiguredBindingRoute => ({ bindingResolution: null, - configuredBinding: null, route, }), ), - mockEnsureConfiguredBindingRouteReady: vi.fn(async () => ({ ok: true as const })), + mockEnsureConfiguredBindingRouteReady: vi.fn( + async (_params?: unknown): Promise => ({ ok: true }), + ), mockResolveBoundConversation: vi.fn(() => null as BoundConversation), mockTouchBinding: vi.fn(), })); @@ -213,7 +242,8 @@ vi.mock("openclaw/plugin-sdk/conversation-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, - resolveConfiguredBindingRoute: (params: unknown) => mockResolveConfiguredBindingRoute(params), + resolveConfiguredBindingRoute: (params: unknown) => + mockResolveConfiguredBindingRoute(params as { route: ResolvedAgentRoute }), ensureConfiguredBindingRouteReady: (params: unknown) => mockEnsureConfiguredBindingRouteReady(params), getSessionBindingService: () => ({ @@ -243,13 +273,16 @@ async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessa describe("handleFeishuMessage ACP routing", () => { beforeEach(() => { vi.clearAllMocks(); - mockResolveConfiguredBindingRoute - .mockReset() - .mockImplementation(({ route }: { route: NonNullable["route"] }) => ({ - bindingResolution: null, - configuredBinding: null, + mockResolveConfiguredBindingRoute.mockReset().mockImplementation( + ({ route, - })); + }: { + route: NonNullable["route"]; + }): ConfiguredBindingRoute => ({ + bindingResolution: null, + route, + }), + ); mockEnsureConfiguredBindingRouteReady.mockReset().mockResolvedValue({ ok: true }); mockResolveBoundConversation.mockReset().mockReturnValue(null); mockTouchBinding.mockReset(); @@ -279,12 +312,12 @@ describe("handleFeishuMessage ACP routing", () => { reply: { resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock, formatAgentEnvelope: vi.fn((params: { body: string }) => params.body), - finalizeInboundContext: finalizeInboundContextMock, + finalizeInboundContext: finalizeInboundContextMock as never, dispatchReplyFromConfig: vi.fn().mockResolvedValue({ queuedFinal: false, counts: { final: 1 }, }), - withReplyDispatcher: withReplyDispatcherMock, + withReplyDispatcher: withReplyDispatcherMock as never, }, commands: { shouldComputeCommandAuthorized: vi.fn(() => false), @@ -399,7 +432,10 @@ describe("handleFeishuMessage ACP routing", () => { }); describe("handleFeishuMessage command authorization", () => { - const mockFinalizeInboundContext = vi.fn((ctx: unknown) => ctx); + const mockFinalizeInboundContext = vi.fn((ctx: Record) => ({ + ...ctx, + CommandAuthorized: typeof ctx.CommandAuthorized === "boolean" ? ctx.CommandAuthorized : false, + })); const mockDispatchReplyFromConfig = vi .fn() .mockResolvedValue({ queuedFinal: false, counts: { final: 1 } }); @@ -441,13 +477,16 @@ describe("handleFeishuMessage command authorization", () => { mockListFeishuThreadMessages.mockReset().mockResolvedValue([]); mockReadSessionUpdatedAt.mockReturnValue(undefined); mockResolveStorePath.mockReturnValue("/tmp/feishu-sessions.json"); - mockResolveConfiguredBindingRoute - .mockReset() - .mockImplementation(({ route }: { route: NonNullable["route"] }) => ({ - bindingResolution: null, - configuredBinding: null, + mockResolveConfiguredBindingRoute.mockReset().mockImplementation( + ({ route, - })); + }: { + route: NonNullable["route"]; + }): ConfiguredBindingRoute => ({ + bindingResolution: null, + route, + }), + ); mockEnsureConfiguredBindingRouteReady.mockReset().mockResolvedValue({ ok: true }); mockResolveBoundConversation.mockReset().mockReturnValue(null); mockTouchBinding.mockReset(); @@ -476,9 +515,9 @@ describe("handleFeishuMessage command authorization", () => { reply: { resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock, formatAgentEnvelope: vi.fn((params: { body: string }) => params.body), - finalizeInboundContext: mockFinalizeInboundContext, + finalizeInboundContext: mockFinalizeInboundContext as never, dispatchReplyFromConfig: mockDispatchReplyFromConfig, - withReplyDispatcher: mockWithReplyDispatcher, + withReplyDispatcher: mockWithReplyDispatcher as never, }, commands: { shouldComputeCommandAuthorized: mockShouldComputeCommandAuthorized, diff --git a/extensions/feishu/src/card-ux-launcher.test.ts b/extensions/feishu/src/card-ux-launcher.test.ts index c38e7da0f73..40efeb02d0f 100644 --- a/extensions/feishu/src/card-ux-launcher.test.ts +++ b/extensions/feishu/src/card-ux-launcher.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; +import { createRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; import { createQuickActionLauncherCard, @@ -86,7 +87,7 @@ describe("feishu quick-action launcher", () => { it("falls back to legacy menu handling when launcher send fails", async () => { sendCardFeishuMock.mockRejectedValueOnce(new Error("network")); - const runtime: RuntimeEnv = { log: vi.fn() }; + const runtime: RuntimeEnv = createRuntimeEnv(); const handled = await maybeHandleFeishuQuickActionMenu({ cfg, diff --git a/extensions/feishu/src/chat.test.ts b/extensions/feishu/src/chat.test.ts index f154389edbd..f704ed6be27 100644 --- a/extensions/feishu/src/chat.test.ts +++ b/extensions/feishu/src/chat.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js"; +import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { OpenClawPluginApi } from "../runtime-api.js"; import { registerFeishuChatTools } from "./chat.js"; @@ -22,8 +23,8 @@ describe("registerFeishuChatTools", () => { name: "Feishu Test", source: "local", config: params.config, - runtime: { log: vi.fn(), error: vi.fn() }, - logger: { debug: vi.fn(), info: vi.fn() }, + runtime: createPluginRuntimeMock(), + logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, registerTool: params.registerTool, }); } diff --git a/extensions/feishu/src/client.test.ts b/extensions/feishu/src/client.test.ts index 68ea308f14d..c1bb208e768 100644 --- a/extensions/feishu/src/client.test.ts +++ b/extensions/feishu/src/client.test.ts @@ -1,6 +1,8 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createTestPluginApi } from "../../../test/helpers/extensions/plugin-api.js"; +import { createPluginRuntimeMock } from "../../../test/helpers/extensions/plugin-runtime-mock.js"; import type { OpenClawPluginApi } from "../runtime-api.js"; +import { FeishuConfigSchema } from "./config-schema.js"; import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js"; type CreateFeishuClient = typeof import("./client.js").createFeishuClient; @@ -100,13 +102,18 @@ const baseAccount: ResolvedFeishuAccount = { appId: "app_123", appSecret: "secret_123", // pragma: allowlist secret domain: "feishu", - config: {}, + config: FeishuConfigSchema.parse({}), }; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } +type HttpInstanceLike = { + get: (url: string, options?: Record) => Promise; + post: (url: string, body?: unknown, options?: Record) => Promise; +}; + function readCallOptions( mock: { mock: { calls: unknown[][] } }, index = -1, @@ -191,9 +198,19 @@ afterEach(() => { }); describe("createFeishuClient HTTP timeout", () => { - const getLastClientHttpInstance = () => { + const getLastClientHttpInstance = (): HttpInstanceLike | undefined => { const httpInstance = readCallOptions(clientCtorMock).httpInstance; - return isRecord(httpInstance) ? httpInstance : undefined; + if ( + isRecord(httpInstance) && + typeof httpInstance.get === "function" && + typeof httpInstance.post === "function" + ) { + return { + get: httpInstance.get as HttpInstanceLike["get"], + post: httpInstance.post as HttpInstanceLike["post"], + }; + } + return undefined; }; const expectGetCallTimeout = async (timeout: number) => { @@ -218,7 +235,7 @@ describe("createFeishuClient HTTP timeout", () => { const httpInstance = getLastClientHttpInstance(); expect(httpInstance).toBeDefined(); - await httpInstance?.post?.( + await httpInstance?.post( "https://example.com/api", { data: 1 }, { headers: { "X-Custom": "yes" } }, @@ -237,7 +254,7 @@ describe("createFeishuClient HTTP timeout", () => { 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", @@ -323,7 +340,7 @@ describe("createFeishuClient HTTP timeout", () => { expect(clientCtorMock.mock.calls.length).toBe(2); const httpInstance = getLastClientHttpInstance(); expect(httpInstance).toBeDefined(); - await httpInstance?.get?.("https://example.com/api"); + await httpInstance?.get("https://example.com/api"); expect(mockBaseHttpInstance.get).toHaveBeenCalledWith( "https://example.com/api", @@ -340,7 +357,7 @@ describe("feishu plugin register", () => { id: "feishu-test", name: "Feishu Test", source: "local", - runtime: { log: vi.fn() }, + runtime: createPluginRuntimeMock(), on: vi.fn(), config: {}, registerChannel, diff --git a/extensions/feishu/src/docx-batch-insert.test.ts b/extensions/feishu/src/docx-batch-insert.test.ts index 79458b95896..2b3f9f39db0 100644 --- a/extensions/feishu/src/docx-batch-insert.test.ts +++ b/extensions/feishu/src/docx-batch-insert.test.ts @@ -3,20 +3,19 @@ import { describe, expect, it, vi } from "vitest"; import { BATCH_SIZE, insertBlocksInBatches } from "./docx-batch-insert.js"; import type { FeishuDocxBlock } from "./docx-types.js"; +type InsertBlocksClient = Parameters[0]; type DocxDescendantCreate = Lark.Client["docx"]["documentBlockDescendant"]["create"]; type DocxDescendantCreateParams = Parameters[0]; type DocxDescendantCreateResponse = Awaited>; -function createDocxDescendantClient( - create: (params: DocxDescendantCreateParams) => Promise, -): Pick { +function createDocxDescendantClient(create: DocxDescendantCreate): InsertBlocksClient { return { docx: { documentBlockDescendant: { create, }, }, - }; + } as InsertBlocksClient; } function createCountingIterable(values: T[]) { @@ -40,13 +39,18 @@ describe("insertBlocksInBatches", () => { block_type: 2, })); const counting = createCountingIterable(blocks); - const createMock = vi.fn(async ({ data }: { data: { children_id: string[] } }) => ({ - code: 0, - data: { - children: data.children_id.map((id) => ({ block_id: id })), - }, - })); - const client = createDocxDescendantClient(createMock); + const createMock = vi.fn( + async (params?: DocxDescendantCreateParams): Promise => ({ + code: 0, + data: { + children: (params?.data?.children_id ?? []).map((id) => ({ + block_id: id, + block_type: 2, + })), + }, + }), + ); + const client = createDocxDescendantClient((params) => createMock(params)); const result = await insertBlocksInBatches( client, @@ -64,18 +68,17 @@ describe("insertBlocksInBatches", () => { it("keeps nested descendants grouped with their root blocks", async () => { const createMock = vi.fn( - async ({ - data, - }: { - data: { children_id: string[]; descendants: Array<{ block_id: string }> }; - }) => ({ + async (params?: DocxDescendantCreateParams): Promise => ({ code: 0, data: { - children: data.children_id.map((id) => ({ block_id: id })), + children: (params?.data?.children_id ?? []).map((id) => ({ + block_id: id, + block_type: 2, + })), }, }), ); - const client = createDocxDescendantClient(createMock); + const client = createDocxDescendantClient((params) => createMock(params)); const blocks: FeishuDocxBlock[] = [ { block_id: "root_a", block_type: 1, children: ["child_a"] }, { block_id: "child_a", block_type: 2 }, @@ -88,9 +91,7 @@ describe("insertBlocksInBatches", () => { expect(createMock).toHaveBeenCalledTimes(1); expect(createMock.mock.calls[0]?.[0]?.data.children_id).toEqual(["root_a", "root_b"]); expect( - createMock.mock.calls[0]?.[0]?.data.descendants.map( - (block: { block_id: string }) => block.block_id, - ), + createMock.mock.calls[0]?.[0]?.data.descendants.map((block) => block.block_id ?? ""), ).toEqual(["root_a", "child_a", "root_b", "child_b"]); }); }); diff --git a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts index eeae4a80412..54cc5e70dab 100644 --- a/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts +++ b/extensions/feishu/src/monitor.broadcast.reply-once.lifecycle.test.ts @@ -12,6 +12,7 @@ import { } from "../../../test/helpers/extensions/feishu-lifecycle.js"; import { createNonExitingRuntimeEnv } from "../../../test/helpers/extensions/runtime-env.js"; import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js"; +import { FeishuConfigSchema } from "./config-schema.js"; import { getFeishuLifecycleTestMocks } from "./lifecycle.test-support.js"; import type { FeishuConfig, ResolvedFeishuAccount } from "./types.js"; @@ -88,7 +89,7 @@ function createLifecycleConfig(): ClawdbotConfig { } function createLifecycleAccount(accountId: "account-A" | "account-B"): ResolvedFeishuAccount { - const config: FeishuConfig = { + const config: FeishuConfig = FeishuConfigSchema.parse({ enabled: true, connectionMode: "websocket", groupPolicy: "open", @@ -99,7 +100,7 @@ function createLifecycleAccount(accountId: "account-A" | "account-B"): ResolvedF requireMention: false, }, }, - }; + }); return { accountId, selectionSource: "explicit", diff --git a/extensions/feishu/src/policy.test.ts b/extensions/feishu/src/policy.test.ts index 8acab38f2e5..1fd8f0e4e94 100644 --- a/extensions/feishu/src/policy.test.ts +++ b/extensions/feishu/src/policy.test.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; import { describe, expect, it } from "vitest"; +import { FeishuConfigSchema } from "./config-schema.js"; import { isFeishuGroupAllowed, resolveFeishuAllowlistMatch, @@ -88,12 +89,12 @@ describe("resolveFeishuReplyPolicy", () => { describe("resolveFeishuGroupConfig", () => { it("falls back to wildcard group config when direct match is missing", () => { - const cfg: FeishuConfig = { + const cfg: FeishuConfig = FeishuConfigSchema.parse({ groups: { "*": { requireMention: false }, "oc-explicit": { requireMention: true }, }, - }; + }); const resolved = resolveFeishuGroupConfig({ cfg, @@ -104,12 +105,12 @@ describe("resolveFeishuGroupConfig", () => { }); it("prefers exact group config over wildcard", () => { - const cfg: FeishuConfig = { + const cfg: FeishuConfig = FeishuConfigSchema.parse({ groups: { "*": { requireMention: false }, "oc-explicit": { requireMention: true }, }, - }; + }); const resolved = resolveFeishuGroupConfig({ cfg, @@ -120,12 +121,12 @@ describe("resolveFeishuGroupConfig", () => { }); it("keeps case-insensitive matching for explicit group ids", () => { - const cfg: FeishuConfig = { + const cfg: FeishuConfig = FeishuConfigSchema.parse({ groups: { "*": { requireMention: false }, OC_UPPER: { requireMention: true }, }, - }; + }); const resolved = resolveFeishuGroupConfig({ cfg, diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index 8d40e18413c..f1d77c5c9ca 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -511,17 +511,17 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { await options.deliver({ text: "answer part final" }, { kind: "final" }); expect(streamingInstances).toHaveLength(1); - const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) => c[0]); - const reasoningUpdate = updateCalls.find((c: string) => c.includes("Thinking")); + const updateCalls = streamingInstances[0].update.mock.calls.map((c: unknown[]) => + String(c[0] ?? ""), + ); + const reasoningUpdate = updateCalls.find((c) => c.includes("Thinking")); expect(reasoningUpdate).toContain("> 💭 **Thinking**"); // formatReasoningPrefix strips "Reasoning:" prefix and italic markers expect(reasoningUpdate).toContain("> thinking step"); expect(reasoningUpdate).not.toContain("Reasoning:"); expect(reasoningUpdate).not.toMatch(/> _.*_/); - const combinedUpdate = updateCalls.find( - (c: string) => c.includes("Thinking") && c.includes("---"), - ); + const combinedUpdate = updateCalls.find((c) => c.includes("Thinking") && c.includes("---")); expect(combinedUpdate).toBeDefined(); expect(streamingInstances[0].close).toHaveBeenCalledTimes(1); diff --git a/extensions/feishu/src/tool-factory-test-harness.ts b/extensions/feishu/src/tool-factory-test-harness.ts index 4c564e25052..9fcf45107be 100644 --- a/extensions/feishu/src/tool-factory-test-harness.ts +++ b/extensions/feishu/src/tool-factory-test-harness.ts @@ -8,7 +8,10 @@ type ToolFactoryLike = (ctx: ToolContextLike) => AnyAgentTool | AnyAgentTool[] | export type ToolLike = { name: string; - execute: (toolCallId: string, params: unknown) => Promise | unknown; + execute: ( + toolCallId: string, + params: unknown, + ) => Promise<{ details: Record }> | { details: Record }; }; type RegisteredTool = {